├── src └── meilisearch_mcp │ ├── __init__.py │ ├── __version__.py │ ├── __main__.py │ ├── keys.py │ ├── settings.py │ ├── indexes.py │ ├── tasks.py │ ├── monitoring.py │ ├── chat.py │ ├── logging.py │ ├── client.py │ ├── documents.py │ └── server.py ├── requirements-dev.txt ├── tests ├── test_server.py ├── test_user_agent.py ├── test_docker_integration.py ├── test_chat.py └── test_mcp_client.py ├── Dockerfile ├── pyproject.toml ├── .dockerignore ├── LICENSE ├── .github └── workflows │ ├── claude.yml │ ├── test.yml │ └── publish.yml ├── .gitignore ├── CLAUDE.md └── README.md /src/meilisearch_mcp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.5.0" 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest>=8.4.1 2 | pytest-asyncio>=1.1.0 3 | black>=25.1.0 4 | mcp>=1.12.4 -------------------------------------------------------------------------------- /src/meilisearch_mcp/__main__.py: -------------------------------------------------------------------------------- 1 | from .server import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.meilisearch_mcp.server import create_server 3 | 4 | 5 | def test_server_creation(): 6 | """Test that we can create a server instance""" 7 | server = create_server() 8 | assert server is not None 9 | assert server.meili_client is not None 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Python 3.12 slim image for smaller size 2 | FROM python:3.13-slim 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Install system dependencies 8 | RUN apt-get update && apt-get dist-upgrade -y && \ 9 | apt-get install -y --no-install-recommends curl && \ 10 | rm -rf /var/lib/apt/lists/* 11 | 12 | # Install uv for faster Python package management 13 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh 14 | ENV PATH="/root/.local/bin:${PATH}" 15 | 16 | # Copy project files 17 | COPY pyproject.toml README.md ./ 18 | COPY src/ ./src/ 19 | 20 | # Install the package 21 | RUN uv pip install --system . 22 | 23 | # Set default environment variables 24 | ENV MEILI_HTTP_ADDR=http://meilisearch:7700 25 | ENV MEILI_MASTER_KEY="" 26 | 27 | # Run the MCP server 28 | CMD ["uv", "run", "python", "-m", "src.meilisearch_mcp"] -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "meilisearch-mcp" 3 | version = "0.6.0" 4 | description = "MCP server for Meilisearch" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "meilisearch>=0.37.0", 8 | "mcp>=1.12.4", 9 | "httpx>=0.28.1", 10 | "pydantic>=2.11.7", 11 | ] 12 | 13 | [build-system] 14 | requires = ["setuptools>=42"] 15 | build-backend = "setuptools.build_meta" 16 | 17 | [project.scripts] 18 | meilisearch-mcp = "meilisearch_mcp.server:main" 19 | 20 | [tool.hatch.build.targets.wheel] 21 | packages = ["src/meilisearch_mcp"] 22 | 23 | [tool.pytest.ini_options] 24 | pythonpath = [ 25 | "." 26 | ] 27 | testpaths = [ 28 | "tests" 29 | ] 30 | asyncio_mode = "auto" 31 | asyncio_default_fixture_loop_scope = "function" 32 | 33 | [tool.black] 34 | line-length = 88 35 | target-version = ['py310'] 36 | include = '\.pyi?$' 37 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Python cache 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual environments 24 | .venv/ 25 | venv/ 26 | ENV/ 27 | env/ 28 | 29 | # Testing 30 | .pytest_cache/ 31 | .coverage 32 | htmlcov/ 33 | .tox/ 34 | .hypothesis/ 35 | 36 | # IDE 37 | .vscode/ 38 | .idea/ 39 | *.swp 40 | *.swo 41 | *~ 42 | 43 | # Git 44 | .git/ 45 | .gitignore 46 | 47 | # Documentation 48 | docs/ 49 | *.md 50 | !README.md 51 | 52 | # CI/CD 53 | .github/ 54 | 55 | # Development files 56 | .env 57 | .env.* 58 | CLAUDE.md 59 | 60 | # Logs 61 | logs/ 62 | *.log 63 | 64 | # macOS 65 | .DS_Store 66 | 67 | # Test data 68 | tests/ 69 | data.ms/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Meilisearch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: write 24 | issues: write 25 | id-token: write 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 1 31 | 32 | - name: Run Claude Code 33 | id: claude 34 | uses: anthropics/claude-code-action@beta 35 | with: 36 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 37 | -------------------------------------------------------------------------------- /tests/test_user_agent.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch, MagicMock 3 | from src.meilisearch_mcp.client import MeilisearchClient 4 | from src.meilisearch_mcp.__version__ import __version__ 5 | 6 | 7 | def test_meilisearch_client_sets_custom_user_agent(): 8 | """Test that MeilisearchClient initializes with custom user agent""" 9 | with patch("src.meilisearch_mcp.client.Client") as mock_client: 10 | # Create a MeilisearchClient instance 11 | client = MeilisearchClient(url="http://localhost:7700", api_key="test_key") 12 | 13 | # Verify that Client was called with the correct parameters 14 | mock_client.assert_called_once_with( 15 | "http://localhost:7700", 16 | "test_key", 17 | client_agents=("meilisearch-mcp", f"v{__version__}"), 18 | ) 19 | 20 | 21 | def test_user_agent_includes_correct_version(): 22 | """Test that the user agent includes the correct version from __version__.py""" 23 | with patch("src.meilisearch_mcp.client.Client") as mock_client: 24 | client = MeilisearchClient() 25 | 26 | # Extract the client_agents parameter from the call 27 | call_args = mock_client.call_args 28 | client_agents = call_args[1]["client_agents"] 29 | 30 | # Verify format and version 31 | assert client_agents[0] == "meilisearch-mcp" 32 | assert client_agents[1] == "v0.5.0" 33 | assert client_agents[1] == f"v{__version__}" 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | meilisearch: 15 | image: getmeili/meilisearch:v1.16 16 | ports: 17 | - 7700:7700 18 | env: 19 | MEILI_MASTER_KEY: test_master_key 20 | MEILI_ENV: development 21 | options: >- 22 | --health-cmd "curl -f http://localhost:7700/health || exit 1" 23 | --health-interval 30s 24 | --health-timeout 10s 25 | --health-retries 5 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.10' 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install -r requirements-dev.txt 39 | pip install -e . 40 | 41 | - name: Format code with black 42 | run: | 43 | black src/ 44 | black tests/ 45 | 46 | - name: Wait for Meilisearch to be ready 47 | run: | 48 | timeout 60 bash -c 'until curl -f http://localhost:7700/health; do sleep 2; done' 49 | 50 | - name: Run tests 51 | env: 52 | MEILI_HTTP_ADDR: http://localhost:7700 53 | MEILI_MASTER_KEY: test_master_key 54 | run: | 55 | pytest tests/ -v -------------------------------------------------------------------------------- /src/meilisearch_mcp/keys.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from meilisearch import Client 3 | from datetime import datetime 4 | 5 | 6 | class KeyManager: 7 | """Manage Meilisearch API keys""" 8 | 9 | def __init__(self, client: Client): 10 | self.client = client 11 | 12 | def get_keys(self, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 13 | """Get list of API keys""" 14 | try: 15 | return self.client.get_keys(parameters) 16 | except Exception as e: 17 | raise Exception(f"Failed to get keys: {str(e)}") 18 | 19 | def get_key(self, key: str) -> Dict[str, Any]: 20 | """Get information about a specific key""" 21 | try: 22 | return self.client.get_key(key) 23 | except Exception as e: 24 | raise Exception(f"Failed to get key: {str(e)}") 25 | 26 | def create_key(self, options: Dict[str, Any]) -> Dict[str, Any]: 27 | """Create a new API key""" 28 | try: 29 | return self.client.create_key(options) 30 | except Exception as e: 31 | raise Exception(f"Failed to create key: {str(e)}") 32 | 33 | def update_key(self, key: str, options: Dict[str, Any]) -> Dict[str, Any]: 34 | """Update an existing API key""" 35 | try: 36 | return self.client.update_key(key, options) 37 | except Exception as e: 38 | raise Exception(f"Failed to update key: {str(e)}") 39 | 40 | def delete_key(self, key: str) -> None: 41 | """Delete an API key""" 42 | try: 43 | return self.client.delete_key(key) 44 | except Exception as e: 45 | raise Exception(f"Failed to delete key: {str(e)}") 46 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from meilisearch import Client 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class SearchSettings: 8 | displayedAttributes: Optional[List[str]] = None 9 | searchableAttributes: Optional[List[str]] = None 10 | filterableAttributes: Optional[List[str]] = None 11 | sortableAttributes: Optional[List[str]] = None 12 | rankingRules: Optional[List[str]] = None 13 | stopWords: Optional[List[str]] = None 14 | synonyms: Optional[Dict[str, List[str]]] = None 15 | distinctAttribute: Optional[str] = None 16 | typoTolerance: Optional[Dict[str, Any]] = None 17 | faceting: Optional[Dict[str, Any]] = None 18 | pagination: Optional[Dict[str, Any]] = None 19 | 20 | 21 | class SettingsManager: 22 | """Manage Meilisearch index settings""" 23 | 24 | def __init__(self, client: Client): 25 | self.client = client 26 | 27 | def get_settings(self, index_uid: str) -> Dict[str, Any]: 28 | """Get all settings for an index""" 29 | try: 30 | index = self.client.index(index_uid) 31 | return index.get_settings() 32 | except Exception as e: 33 | raise Exception(f"Failed to get settings: {str(e)}") 34 | 35 | def update_settings( 36 | self, index_uid: str, settings: Dict[str, Any] 37 | ) -> Dict[str, Any]: 38 | """Update settings for an index""" 39 | try: 40 | index = self.client.index(index_uid) 41 | return index.update_settings(settings) 42 | except Exception as e: 43 | raise Exception(f"Failed to update settings: {str(e)}") 44 | 45 | def reset_settings(self, index_uid: str) -> Dict[str, Any]: 46 | """Reset settings to default values""" 47 | try: 48 | index = self.client.index(index_uid) 49 | return index.reset_settings() 50 | except Exception as e: 51 | raise Exception(f"Failed to reset settings: {str(e)}") 52 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/indexes.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional, List 2 | from dataclasses import dataclass 3 | from meilisearch import Client 4 | 5 | 6 | @dataclass 7 | class IndexConfig: 8 | """Index configuration model""" 9 | 10 | uid: str 11 | primary_key: Optional[str] = None 12 | 13 | 14 | class IndexManager: 15 | """Manage Meilisearch indexes""" 16 | 17 | def __init__(self, client: Client): 18 | self.client = client 19 | 20 | def create_index( 21 | self, uid: str, primary_key: Optional[str] = None 22 | ) -> Dict[str, Any]: 23 | """Create a new index""" 24 | try: 25 | return self.client.create_index(uid, {"primaryKey": primary_key}) 26 | except Exception as e: 27 | raise Exception(f"Failed to create index: {str(e)}") 28 | 29 | def get_index(self, uid: str) -> Dict[str, Any]: 30 | """Get index information""" 31 | try: 32 | return self.client.get_index(uid) 33 | except Exception as e: 34 | raise Exception(f"Failed to get index: {str(e)}") 35 | 36 | def list_indexes(self) -> List[Dict[str, Any]]: 37 | """List all indexes""" 38 | try: 39 | return self.client.get_indexes() 40 | except Exception as e: 41 | raise Exception(f"Failed to list indexes: {str(e)}") 42 | 43 | def delete_index(self, uid: str) -> Dict[str, Any]: 44 | """Delete an index""" 45 | try: 46 | return self.client.delete_index(uid) 47 | except Exception as e: 48 | raise Exception(f"Failed to delete index: {str(e)}") 49 | 50 | def update_index(self, uid: str, primary_key: str) -> Dict[str, Any]: 51 | """Update index primary key""" 52 | try: 53 | return self.client.update_index(uid, {"primaryKey": primary_key}) 54 | except Exception as e: 55 | raise Exception(f"Failed to update index: {str(e)}") 56 | 57 | def swap_indexes(self, indexes: List[List[str]]) -> Dict[str, Any]: 58 | """Swap indexes""" 59 | try: 60 | return self.client.swap_indexes(indexes) 61 | except Exception as e: 62 | raise Exception(f"Failed to swap indexes: {str(e)}") 63 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/tasks.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from meilisearch import Client 3 | from datetime import datetime 4 | 5 | 6 | def serialize_task_results(obj: Any) -> Any: 7 | """Serialize task results into JSON-compatible format""" 8 | if hasattr(obj, "__dict__"): 9 | return {k: serialize_task_results(v) for k, v in obj.__dict__.items()} 10 | elif isinstance(obj, (list, tuple)): 11 | return [serialize_task_results(item) for item in obj] 12 | elif isinstance(obj, datetime): 13 | return obj.isoformat() 14 | return obj 15 | 16 | 17 | class TaskManager: 18 | def __init__(self, client: Client): 19 | """Initialize TaskManager with Meilisearch client""" 20 | self.client = client 21 | 22 | def get_task(self, task_uid: int) -> Dict[str, Any]: 23 | """Get information about a specific task""" 24 | try: 25 | task = self.client.get_task(task_uid) 26 | return serialize_task_results(task) 27 | except Exception as e: 28 | raise Exception(f"Failed to get task: {str(e)}") 29 | 30 | def get_tasks(self, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 31 | """Get list of tasks with optional filters""" 32 | try: 33 | tasks = self.client.get_tasks(parameters) 34 | return serialize_task_results(tasks) 35 | except Exception as e: 36 | raise Exception(f"Failed to get tasks: {str(e)}") 37 | 38 | def cancel_tasks(self, query_parameters: Dict[str, Any]) -> Dict[str, Any]: 39 | """Cancel tasks based on query parameters""" 40 | try: 41 | result = self.client.cancel_tasks(query_parameters) 42 | return serialize_task_results(result) 43 | except Exception as e: 44 | raise Exception(f"Failed to cancel tasks: {str(e)}") 45 | 46 | def delete_tasks(self, query_parameters: Dict[str, Any]) -> Dict[str, Any]: 47 | """Delete tasks based on query parameters""" 48 | try: 49 | result = self.client.delete_tasks(query_parameters) 50 | return serialize_task_results(result) 51 | except Exception as e: 52 | raise Exception(f"Failed to delete tasks: {str(e)}") 53 | -------------------------------------------------------------------------------- /tests/test_docker_integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Integration tests for Docker image build. 3 | 4 | These tests verify that the Docker image can be built successfully. 5 | """ 6 | 7 | import subprocess 8 | import pytest 9 | import shutil 10 | 11 | 12 | # Check if Docker is available 13 | def docker_available(): 14 | """Check if Docker is available on the system.""" 15 | if not shutil.which("docker"): 16 | return False 17 | # Try to run docker version to ensure it's working 18 | try: 19 | result = subprocess.run( 20 | ["docker", "version"], capture_output=True, text=True, timeout=5 21 | ) 22 | return result.returncode == 0 23 | except (subprocess.TimeoutExpired, FileNotFoundError): 24 | return False 25 | 26 | 27 | # Skip all tests in this module if Docker is not available 28 | pytestmark = pytest.mark.skipif( 29 | not docker_available(), reason="Docker not available on this system" 30 | ) 31 | 32 | 33 | def test_docker_build(): 34 | """Test that the Docker image can be built successfully.""" 35 | result = subprocess.run( 36 | ["docker", "build", "-t", "meilisearch-mcp-test", "."], 37 | capture_output=True, 38 | text=True, 39 | ) 40 | assert result.returncode == 0, f"Docker build failed: {result.stderr}" 41 | 42 | 43 | def test_docker_image_runs(): 44 | """Test that the Docker image can run and show help.""" 45 | # First build the image 46 | build_result = subprocess.run( 47 | ["docker", "build", "-t", "meilisearch-mcp-test", "."], 48 | capture_output=True, 49 | text=True, 50 | ) 51 | if build_result.returncode != 0: 52 | pytest.skip(f"Docker build failed: {build_result.stderr}") 53 | 54 | # Try to run the container and check it starts 55 | result = subprocess.run( 56 | [ 57 | "docker", 58 | "run", 59 | "--rm", 60 | "-e", 61 | "MEILI_HTTP_ADDR=http://localhost:7700", 62 | "-e", 63 | "MEILI_MASTER_KEY=test", 64 | "meilisearch-mcp-test", 65 | "python", 66 | "-c", 67 | "import src.meilisearch_mcp; print('MCP module loaded successfully')", 68 | ], 69 | capture_output=True, 70 | text=True, 71 | timeout=30, 72 | ) 73 | 74 | assert result.returncode == 0, f"Docker run failed: {result.stderr}" 75 | assert "MCP module loaded successfully" in result.stdout 76 | 77 | 78 | if __name__ == "__main__": 79 | # Run tests 80 | pytest.main([__file__, "-v"]) 81 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/monitoring.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from meilisearch import Client 3 | from dataclasses import dataclass 4 | from datetime import datetime 5 | import json 6 | 7 | 8 | @dataclass 9 | class HealthStatus: 10 | """Detailed health status information""" 11 | 12 | is_healthy: bool 13 | database_size: int 14 | last_update: datetime 15 | indexes_count: int 16 | indexes_info: List[Dict[str, Any]] 17 | 18 | 19 | @dataclass 20 | class IndexMetrics: 21 | """Detailed index metrics""" 22 | 23 | number_of_documents: int 24 | field_distribution: Dict[str, int] 25 | is_indexing: bool 26 | index_size: Optional[int] = None 27 | 28 | 29 | class MonitoringManager: 30 | """Enhanced monitoring and statistics for Meilisearch""" 31 | 32 | def __init__(self, client: Client): 33 | self.client = client 34 | 35 | def get_health_status(self) -> HealthStatus: 36 | """Get comprehensive health status""" 37 | try: 38 | # Get various stats to build health picture 39 | stats = self.client.get_all_stats() 40 | indexes = self.client.get_indexes() 41 | 42 | indexes_info = [] 43 | for index in indexes: 44 | index_stats = self.client.index(index.uid).get_stats() 45 | indexes_info.append( 46 | { 47 | "uid": index.uid, 48 | "documents_count": index_stats["numberOfDocuments"], 49 | "is_indexing": index_stats["isIndexing"], 50 | } 51 | ) 52 | 53 | return HealthStatus( 54 | is_healthy=True, 55 | database_size=stats["databaseSize"], 56 | last_update=datetime.fromisoformat( 57 | stats["lastUpdate"].replace("Z", "+00:00") 58 | ), 59 | indexes_count=len(indexes), 60 | indexes_info=indexes_info, 61 | ) 62 | except Exception as e: 63 | raise Exception(f"Failed to get health status: {str(e)}") 64 | 65 | def get_index_metrics(self, index_uid: str) -> IndexMetrics: 66 | """Get detailed metrics for an index""" 67 | try: 68 | index = self.client.index(index_uid) 69 | stats = index.get_stats() 70 | 71 | return IndexMetrics( 72 | number_of_documents=stats["numberOfDocuments"], 73 | field_distribution=stats["fieldDistribution"], 74 | is_indexing=stats["isIndexing"], 75 | index_size=stats.get("indexSize"), 76 | ) 77 | except Exception as e: 78 | raise Exception(f"Failed to get index metrics: {str(e)}") 79 | 80 | def get_system_information(self) -> Dict[str, Any]: 81 | """Get system-level information""" 82 | try: 83 | version = self.client.get_version() 84 | stats = self.client.get_all_stats() 85 | 86 | return { 87 | "version": version, 88 | "database_size": stats["databaseSize"], 89 | "last_update": stats["lastUpdate"], 90 | "indexes": stats["indexes"], 91 | } 92 | except Exception as e: 93 | raise Exception(f"Failed to get system information: {str(e)}") 94 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | check-version: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | version_changed: ${{ steps.check_version.outputs.version_changed }} 13 | current_version: ${{ steps.get_version.outputs.current_version }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 2 18 | 19 | - name: Get current version 20 | id: get_version 21 | run: | 22 | CURRENT_VERSION=$(grep "version = " pyproject.toml | cut -d'"' -f2) 23 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT 24 | 25 | - name: Check if version changed 26 | id: check_version 27 | run: | 28 | if git diff HEAD^ HEAD -- pyproject.toml | grep -q "version = "; then 29 | echo "version_changed=true" >> $GITHUB_OUTPUT 30 | else 31 | echo "version_changed=false" >> $GITHUB_OUTPUT 32 | fi 33 | 34 | build-and-publish-pypi: 35 | needs: check-version 36 | if: needs.check-version.outputs.version_changed == 'true' 37 | runs-on: ubuntu-latest 38 | environment: 39 | name: pypi 40 | url: https://pypi.org/p/meilisearch-mcp 41 | permissions: 42 | id-token: write # IMPORTANT: mandatory for trusted publishing 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Set up Python 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: "3.10" 51 | 52 | - name: Install build dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install build 56 | 57 | - name: Build package 58 | run: python -m build 59 | 60 | - name: Publish to PyPI 61 | uses: pypa/gh-action-pypi-publish@release/v1 62 | with: 63 | verbose: true 64 | print-hash: true 65 | 66 | build-and-publish-docker-latest: 67 | runs-on: ubuntu-latest 68 | 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: Set up QEMU 73 | uses: docker/setup-qemu-action@v3 74 | 75 | - name: Set up Docker Buildx 76 | uses: docker/setup-buildx-action@v3 77 | 78 | - name: Log in to Docker Hub 79 | uses: docker/login-action@v3 80 | with: 81 | username: ${{ secrets.DOCKERHUB_USERNAME }} 82 | password: ${{ secrets.DOCKERHUB_TOKEN }} 83 | 84 | - name: Build and push latest Docker image 85 | uses: docker/build-push-action@v6 86 | with: 87 | context: . 88 | platforms: linux/amd64,linux/arm64 89 | push: true 90 | tags: getmeili/meilisearch-mcp:latest 91 | cache-from: type=gha 92 | cache-to: type=gha,mode=max 93 | 94 | build-and-publish-docker-versioned: 95 | needs: check-version 96 | if: needs.check-version.outputs.version_changed == 'true' 97 | runs-on: ubuntu-latest 98 | 99 | steps: 100 | - uses: actions/checkout@v4 101 | 102 | - name: Set up QEMU 103 | uses: docker/setup-qemu-action@v3 104 | 105 | - name: Set up Docker Buildx 106 | uses: docker/setup-buildx-action@v3 107 | 108 | - name: Log in to Docker Hub 109 | uses: docker/login-action@v3 110 | with: 111 | username: ${{ secrets.DOCKERHUB_USERNAME }} 112 | password: ${{ secrets.DOCKERHUB_TOKEN }} 113 | 114 | - name: Build and push versioned Docker image 115 | uses: docker/build-push-action@v6 116 | with: 117 | context: . 118 | platforms: linux/amd64,linux/arm64 119 | push: true 120 | tags: getmeili/meilisearch-mcp:${{ needs.check-version.outputs.current_version }} 121 | cache-from: type=gha 122 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /src/meilisearch_mcp/chat.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Iterator, List, Optional 2 | 3 | from meilisearch import Client 4 | from meilisearch.errors import MeilisearchApiError 5 | 6 | from .logging import MCPLogger 7 | 8 | logger = MCPLogger() 9 | 10 | 11 | class ChatManager: 12 | def __init__(self, client: Client): 13 | self.client = client 14 | 15 | async def create_chat_completion( 16 | self, 17 | workspace_uid: str, 18 | messages: List[Dict[str, str]], 19 | model: str = "gpt-3.5-turbo", 20 | stream: bool = True, 21 | ) -> str: 22 | try: 23 | logger.info(f"Creating chat completion for workspace: {workspace_uid}") 24 | 25 | # The SDK returns an iterator for streaming responses 26 | response_chunks = [] 27 | for chunk in self.client.create_chat_completion( 28 | workspace_uid=workspace_uid, 29 | messages=messages, 30 | model=model, 31 | stream=stream, 32 | ): 33 | response_chunks.append(chunk) 34 | 35 | # Combine all chunks into a complete response 36 | full_response = self._combine_chunks(response_chunks) 37 | logger.info( 38 | f"Chat completion created successfully for workspace: {workspace_uid}" 39 | ) 40 | return full_response 41 | 42 | except MeilisearchApiError as e: 43 | logger.error(f"Meilisearch API error in create_chat_completion: {e}") 44 | raise 45 | except Exception as e: 46 | logger.error(f"Error in create_chat_completion: {e}") 47 | raise 48 | 49 | def _combine_chunks(self, chunks: List[Dict[str, Any]]) -> str: 50 | """Combine streaming chunks into a single response message.""" 51 | content_parts = [] 52 | for chunk in chunks: 53 | if "choices" in chunk and chunk["choices"]: 54 | choice = chunk["choices"][0] 55 | if "delta" in choice and "content" in choice["delta"]: 56 | content_parts.append(choice["delta"]["content"]) 57 | return "".join(content_parts) 58 | 59 | async def get_chat_workspaces( 60 | self, offset: Optional[int] = None, limit: Optional[int] = None 61 | ) -> Dict[str, Any]: 62 | try: 63 | logger.info(f"Getting chat workspaces (offset={offset}, limit={limit})") 64 | workspaces = self.client.get_chat_workspaces(offset=offset, limit=limit) 65 | logger.info( 66 | f"Retrieved {len(workspaces.get('results', []))} chat workspaces" 67 | ) 68 | return workspaces 69 | except MeilisearchApiError as e: 70 | logger.error(f"Meilisearch API error in get_chat_workspaces: {e}") 71 | raise 72 | except Exception as e: 73 | logger.error(f"Error in get_chat_workspaces: {e}") 74 | raise 75 | 76 | async def get_chat_workspace_settings(self, workspace_uid: str) -> Dict[str, Any]: 77 | try: 78 | logger.info(f"Getting settings for chat workspace: {workspace_uid}") 79 | settings = self.client.get_chat_workspace_settings(workspace_uid) 80 | logger.info(f"Retrieved settings for workspace: {workspace_uid}") 81 | return settings 82 | except MeilisearchApiError as e: 83 | logger.error(f"Meilisearch API error in get_chat_workspace_settings: {e}") 84 | raise 85 | except Exception as e: 86 | logger.error(f"Error in get_chat_workspace_settings: {e}") 87 | raise 88 | 89 | async def update_chat_workspace_settings( 90 | self, workspace_uid: str, settings: Dict[str, Any] 91 | ) -> Dict[str, Any]: 92 | try: 93 | logger.info(f"Updating settings for chat workspace: {workspace_uid}") 94 | updated_settings = self.client.update_chat_workspace_settings( 95 | workspace_uid, settings 96 | ) 97 | logger.info(f"Updated settings for workspace: {workspace_uid}") 98 | return updated_settings 99 | except MeilisearchApiError as e: 100 | logger.error( 101 | f"Meilisearch API error in update_chat_workspace_settings: {e}" 102 | ) 103 | raise 104 | except Exception as e: 105 | logger.error(f"Error in update_chat_workspace_settings: {e}") 106 | raise 107 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import json 4 | from datetime import datetime, timezone 5 | from pathlib import Path 6 | from typing import Optional, Dict, Any 7 | import threading 8 | from queue import Queue 9 | import asyncio 10 | 11 | 12 | class AsyncLogHandler: 13 | """Asynchronous log handler with buffering""" 14 | 15 | def __init__(self, max_buffer: int = 1000): 16 | self.buffer = Queue(maxsize=max_buffer) 17 | self.running = True 18 | self.worker_thread = threading.Thread(target=self._worker) 19 | self.worker_thread.daemon = True 20 | self.worker_thread.start() 21 | 22 | def _worker(self): 23 | """Background worker to process logs""" 24 | while self.running: 25 | try: 26 | record = self.buffer.get(timeout=1.0) 27 | self._write_log(record) 28 | except: 29 | continue 30 | 31 | def _write_log(self, record: Dict[str, Any]): 32 | """Write log record to storage""" 33 | raise NotImplementedError 34 | 35 | def emit(self, record: Dict[str, Any]): 36 | """Add log record to buffer""" 37 | try: 38 | self.buffer.put(record, block=False) 39 | except: 40 | pass # Buffer full, skip log 41 | 42 | def shutdown(self): 43 | """Shutdown the handler""" 44 | self.running = False 45 | self.worker_thread.join() 46 | 47 | 48 | class FileLogHandler(AsyncLogHandler): 49 | """File-based log handler""" 50 | 51 | def __init__(self, log_dir: str): 52 | super().__init__() 53 | self.log_dir = Path(log_dir) 54 | self.log_dir.mkdir(parents=True, exist_ok=True) 55 | self.current_file = None 56 | self._rotate_file() 57 | 58 | def _rotate_file(self): 59 | """Rotate log file daily""" 60 | date_str = datetime.now().strftime("%Y-%m-%d") 61 | self.current_file = self.log_dir / f"meilisearch-mcp-{date_str}.log" 62 | 63 | def _write_log(self, record: Dict[str, Any]): 64 | """Write log record to file""" 65 | current_date = datetime.now().strftime("%Y-%m-%d") 66 | if not self.current_file or current_date not in self.current_file.name: 67 | self._rotate_file() 68 | 69 | with open(self.current_file, "a") as f: 70 | f.write(json.dumps(record) + "\n") 71 | 72 | 73 | class MCPLogger: 74 | """Enhanced MCP logger with structured logging""" 75 | 76 | def __init__(self, name: str = "meilisearch-mcp", log_dir: Optional[str] = None): 77 | self.logger = logging.getLogger(name) 78 | self._setup_logger(log_dir) 79 | 80 | def _setup_logger(self, log_dir: Optional[str]): 81 | """Configure logging with multiple handlers""" 82 | if not self.logger.handlers: 83 | # Console handler 84 | console_handler = logging.StreamHandler(sys.stderr) 85 | formatter = logging.Formatter( 86 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 87 | ) 88 | console_handler.setFormatter(formatter) 89 | self.logger.addHandler(console_handler) 90 | 91 | # File handler for structured logging 92 | if log_dir: 93 | self.file_handler = FileLogHandler(log_dir) 94 | 95 | self.logger.setLevel(logging.INFO) 96 | 97 | def _log(self, level: str, msg: str, **kwargs): 98 | """Create structured log entry""" 99 | log_entry = { 100 | "timestamp": datetime.now(timezone.utc).isoformat(), 101 | "level": level, 102 | "message": msg, 103 | **kwargs, 104 | } 105 | 106 | # Log to console 107 | getattr(self.logger, level.lower())(msg) 108 | 109 | # Log structured data to file 110 | if hasattr(self, "file_handler"): 111 | self.file_handler.emit(log_entry) 112 | 113 | def debug(self, msg: str, **kwargs): 114 | self._log("DEBUG", msg, **kwargs) 115 | 116 | def info(self, msg: str, **kwargs): 117 | self._log("INFO", msg, **kwargs) 118 | 119 | def warning(self, msg: str, **kwargs): 120 | self._log("WARNING", msg, **kwargs) 121 | 122 | def error(self, msg: str, **kwargs): 123 | self._log("ERROR", msg, **kwargs) 124 | 125 | def shutdown(self): 126 | """Clean shutdown of logger""" 127 | if hasattr(self, "file_handler"): 128 | self.file_handler.shutdown() 129 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/client.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from meilisearch import Client 3 | from typing import Optional, Dict, Any, List 4 | 5 | from .indexes import IndexManager 6 | from .documents import DocumentManager 7 | from .tasks import TaskManager 8 | from .settings import SettingsManager 9 | from .keys import KeyManager 10 | from .logging import MCPLogger 11 | from .monitoring import MonitoringManager 12 | from .__version__ import __version__ 13 | 14 | logger = MCPLogger() 15 | 16 | 17 | class MeilisearchClient: 18 | def __init__( 19 | self, url: str = "http://localhost:7700", api_key: Optional[str] = None 20 | ): 21 | """Initialize Meilisearch client""" 22 | self.url = url 23 | self.api_key = api_key 24 | # Add custom user agent to identify this as Meilisearch MCP 25 | self.client = Client( 26 | url, api_key, client_agents=("meilisearch-mcp", f"v{__version__}") 27 | ) 28 | self.indexes = IndexManager(self.client) 29 | self.documents = DocumentManager(self.client) 30 | self.settings = SettingsManager(self.client) 31 | self.tasks = TaskManager(self.client) 32 | self.keys = KeyManager(self.client) 33 | self.monitoring = MonitoringManager(self.client) 34 | 35 | def health_check(self) -> bool: 36 | """Check if Meilisearch is healthy""" 37 | try: 38 | response = self.client.health() 39 | return response.get("status") == "available" 40 | except Exception: 41 | return False 42 | 43 | def get_version(self) -> Dict[str, Any]: 44 | """Get Meilisearch version information""" 45 | return self.client.get_version() 46 | 47 | def get_stats(self) -> Dict[str, Any]: 48 | """Get database stats""" 49 | return self.client.get_all_stats() 50 | 51 | def search( 52 | self, 53 | query: str, 54 | index_uid: Optional[str] = None, 55 | limit: Optional[int] = 20, 56 | offset: Optional[int] = 0, 57 | filter: Optional[str] = None, 58 | sort: Optional[List[str]] = None, 59 | **kwargs, 60 | ) -> Dict[str, Any]: 61 | """ 62 | Search through Meilisearch indices. 63 | If index_uid is provided, search in that specific index. 64 | If not provided, search across all available indices. 65 | """ 66 | try: 67 | # Prepare search parameters, removing None values 68 | search_params = { 69 | "limit": limit if limit is not None else 20, 70 | "offset": offset if offset is not None else 0, 71 | } 72 | 73 | if filter is not None: 74 | search_params["filter"] = filter 75 | if sort is not None: 76 | search_params["sort"] = sort 77 | 78 | # Add any additional parameters 79 | search_params.update({k: v for k, v in kwargs.items() if v is not None}) 80 | 81 | if index_uid: 82 | # Search in specific index 83 | index = self.client.index(index_uid) 84 | return index.search(query, search_params) 85 | else: 86 | # Search across all indices 87 | results = {} 88 | indexes = self.client.get_indexes() 89 | 90 | for index in indexes["results"]: 91 | try: 92 | search_result = index.search(query, search_params) 93 | if search_result["hits"]: # Only include indices with matches 94 | results[index.uid] = search_result 95 | except Exception as e: 96 | logger.warning(f"Failed to search index {index.uid}: {str(e)}") 97 | continue 98 | 99 | return {"multi_index": True, "query": query, "results": results} 100 | 101 | except Exception as e: 102 | raise Exception(f"Search failed: {str(e)}") 103 | 104 | def get_indexes(self) -> Dict[str, Any]: 105 | """Get all indexes""" 106 | indexes = self.client.get_indexes() 107 | # Convert Index objects to serializable dictionaries 108 | serialized_indexes = [] 109 | for index in indexes["results"]: 110 | serialized_indexes.append( 111 | { 112 | "uid": index.uid, 113 | "primaryKey": index.primary_key, 114 | "createdAt": index.created_at, 115 | "updatedAt": index.updated_at, 116 | } 117 | ) 118 | 119 | return { 120 | "results": serialized_indexes, 121 | "offset": indexes["offset"], 122 | "limit": indexes["limit"], 123 | "total": indexes["total"], 124 | } 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Python ### 38 | # Byte-compiled / optimized / DLL files 39 | __pycache__/ 40 | *.py[cod] 41 | *$py.class 42 | 43 | # C extensions 44 | *.so 45 | 46 | # Distribution / packaging 47 | .Python 48 | build/ 49 | develop-eggs/ 50 | dist/ 51 | downloads/ 52 | eggs/ 53 | .eggs/ 54 | lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | wheels/ 60 | share/python-wheels/ 61 | *.egg-info/ 62 | .installed.cfg 63 | *.egg 64 | MANIFEST 65 | 66 | # PyInstaller 67 | # Usually these files are written by a python script from a template 68 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 69 | *.manifest 70 | *.spec 71 | 72 | # Installer logs 73 | pip-log.txt 74 | pip-delete-this-directory.txt 75 | 76 | # Unit test / coverage reports 77 | htmlcov/ 78 | .tox/ 79 | .nox/ 80 | .coverage 81 | .coverage.* 82 | .cache 83 | nosetests.xml 84 | coverage.xml 85 | *.cover 86 | *.py,cover 87 | .hypothesis/ 88 | .pytest_cache/ 89 | cover/ 90 | 91 | # Translations 92 | *.mo 93 | *.pot 94 | 95 | # Django stuff: 96 | *.log 97 | local_settings.py 98 | db.sqlite3 99 | db.sqlite3-journal 100 | 101 | # Flask stuff: 102 | instance/ 103 | .webassets-cache 104 | 105 | # Scrapy stuff: 106 | .scrapy 107 | 108 | # Sphinx documentation 109 | docs/_build/ 110 | 111 | # PyBuilder 112 | .pybuilder/ 113 | target/ 114 | 115 | # Jupyter Notebook 116 | .ipynb_checkpoints 117 | 118 | # IPython 119 | profile_default/ 120 | ipython_config.py 121 | 122 | # pyenv 123 | # For a library or package, you might want to ignore these files since the code is 124 | # intended to run in multiple environments; otherwise, check them in: 125 | # .python-version 126 | 127 | # pipenv 128 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 129 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 130 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 131 | # install all needed dependencies. 132 | #Pipfile.lock 133 | 134 | # poetry 135 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 136 | # This is especially recommended for binary packages to ensure reproducibility, and is more 137 | # commonly ignored for libraries. 138 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 139 | #poetry.lock 140 | 141 | # pdm 142 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 143 | #pdm.lock 144 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 145 | # in version control. 146 | # https://pdm.fming.dev/#use-with-ide 147 | .pdm.toml 148 | 149 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 150 | __pypackages__/ 151 | 152 | # Celery stuff 153 | celerybeat-schedule 154 | celerybeat.pid 155 | 156 | # SageMath parsed files 157 | *.sage.py 158 | 159 | # Environments 160 | .env 161 | .venv 162 | env/ 163 | venv/ 164 | ENV/ 165 | env.bak/ 166 | venv.bak/ 167 | 168 | # Spyder project settings 169 | .spyderproject 170 | .spyproject 171 | 172 | # Rope project settings 173 | .ropeproject 174 | 175 | # mkdocs documentation 176 | /site 177 | 178 | # mypy 179 | .mypy_cache/ 180 | .dmypy.json 181 | dmypy.json 182 | 183 | # Pyre type checker 184 | .pyre/ 185 | 186 | # pytype static type analyzer 187 | .pytype/ 188 | 189 | # Cython debug symbols 190 | cython_debug/ 191 | 192 | # PyCharm 193 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 194 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 195 | # and can be added to the global gitignore or merged into this file. For a more nuclear 196 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 197 | #.idea/ 198 | 199 | ### Python Patch ### 200 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 201 | poetry.toml 202 | 203 | # ruff 204 | .ruff_cache/ 205 | 206 | # LSP config files 207 | pyrightconfig.json 208 | 209 | # End of https://www.toptal.com/developers/gitignore/api/python,macos 210 | .env 211 | 212 | # Meilisearch data directory 213 | data.ms/ 214 | -------------------------------------------------------------------------------- /src/meilisearch_mcp/documents.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional, Union 2 | from meilisearch import Client 3 | 4 | 5 | class DocumentManager: 6 | """Manage documents within Meilisearch indexes""" 7 | 8 | def __init__(self, client: Client): 9 | self.client = client 10 | 11 | def get_documents( 12 | self, 13 | index_uid: str, 14 | offset: Optional[int] = None, 15 | limit: Optional[int] = None, 16 | fields: Optional[List[str]] = None, 17 | ) -> Dict[str, Any]: 18 | """Get documents from an index""" 19 | try: 20 | index = self.client.index(index_uid) 21 | # Build parameters dict, excluding None values to avoid API errors 22 | params = {} 23 | if offset is not None: 24 | params["offset"] = offset 25 | if limit is not None: 26 | params["limit"] = limit 27 | if fields is not None: 28 | params["fields"] = fields 29 | 30 | result = index.get_documents(params if params else {}) 31 | 32 | # Convert meilisearch model objects to JSON-serializable format 33 | if hasattr(result, "__dict__"): 34 | result_dict = result.__dict__.copy() 35 | # Convert individual document objects in results if they exist 36 | if "results" in result_dict and isinstance( 37 | result_dict["results"], list 38 | ): 39 | serialized_results = [] 40 | for doc in result_dict["results"]: 41 | if hasattr(doc, "__dict__"): 42 | # Extract the actual document data 43 | doc_dict = doc.__dict__.copy() 44 | # Look for private attributes that might contain the actual data 45 | for key, value in doc_dict.items(): 46 | if key.startswith("_") and isinstance(value, dict): 47 | # Use the dict content instead of the wrapper 48 | serialized_results.append(value) 49 | break 50 | else: 51 | # If no private dict found, use the object dict directly 52 | serialized_results.append(doc_dict) 53 | else: 54 | serialized_results.append(doc) 55 | result_dict["results"] = serialized_results 56 | return result_dict 57 | else: 58 | return result 59 | except Exception as e: 60 | raise Exception(f"Failed to get documents: {str(e)}") 61 | 62 | def get_document( 63 | self, index_uid: str, document_id: Union[str, int] 64 | ) -> Dict[str, Any]: 65 | """Get a single document""" 66 | try: 67 | index = self.client.index(index_uid) 68 | return index.get_document(document_id) 69 | except Exception as e: 70 | raise Exception(f"Failed to get document: {str(e)}") 71 | 72 | def add_documents( 73 | self, 74 | index_uid: str, 75 | documents: List[Dict[str, Any]], 76 | primary_key: Optional[str] = None, 77 | ) -> Dict[str, Any]: 78 | """Add documents to an index""" 79 | try: 80 | index = self.client.index(index_uid) 81 | return index.add_documents(documents, primary_key) 82 | except Exception as e: 83 | raise Exception(f"Failed to add documents: {str(e)}") 84 | 85 | def update_documents( 86 | self, index_uid: str, documents: List[Dict[str, Any]] 87 | ) -> Dict[str, Any]: 88 | """Update documents in an index""" 89 | try: 90 | index = self.client.index(index_uid) 91 | return index.update_documents(documents) 92 | except Exception as e: 93 | raise Exception(f"Failed to update documents: {str(e)}") 94 | 95 | def delete_document( 96 | self, index_uid: str, document_id: Union[str, int] 97 | ) -> Dict[str, Any]: 98 | """Delete a single document""" 99 | try: 100 | index = self.client.index(index_uid) 101 | return index.delete_document(document_id) 102 | except Exception as e: 103 | raise Exception(f"Failed to delete document: {str(e)}") 104 | 105 | def delete_documents( 106 | self, index_uid: str, document_ids: List[Union[str, int]] 107 | ) -> Dict[str, Any]: 108 | """Delete multiple documents by ID""" 109 | try: 110 | index = self.client.index(index_uid) 111 | return index.delete_documents(document_ids) 112 | except Exception as e: 113 | raise Exception(f"Failed to delete documents: {str(e)}") 114 | 115 | def delete_all_documents(self, index_uid: str) -> Dict[str, Any]: 116 | """Delete all documents in an index""" 117 | try: 118 | index = self.client.index(index_uid) 119 | return index.delete_all_documents() 120 | except Exception as e: 121 | raise Exception(f"Failed to delete all documents: {str(e)}") 122 | -------------------------------------------------------------------------------- /tests/test_chat.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | from unittest.mock import MagicMock, Mock 4 | from meilisearch.errors import MeilisearchApiError 5 | 6 | from src.meilisearch_mcp.server import MeilisearchMCPServer 7 | 8 | 9 | @pytest.fixture 10 | def server(): 11 | return MeilisearchMCPServer(url="http://localhost:7700", api_key="test_key") 12 | 13 | 14 | @pytest.fixture 15 | def setup_mock_chat_client(server): 16 | """Mock the Meilisearch client for chat-related methods""" 17 | # Create a mock client with chat methods 18 | mock_client = MagicMock() 19 | 20 | # Mock create_chat_completion to return an iterator 21 | def mock_chat_completion(*args, **kwargs): 22 | # Simulate streaming response chunks 23 | chunks = [ 24 | {"choices": [{"delta": {"content": "This is "}}]}, 25 | {"choices": [{"delta": {"content": "a test "}}]}, 26 | {"choices": [{"delta": {"content": "response."}}]}, 27 | ] 28 | for chunk in chunks: 29 | yield chunk 30 | 31 | mock_client.create_chat_completion = mock_chat_completion 32 | 33 | # Mock get_chat_workspaces 34 | mock_client.get_chat_workspaces.return_value = { 35 | "results": [ 36 | {"uid": "workspace1", "name": "Customer Support"}, 37 | {"uid": "workspace2", "name": "Documentation"}, 38 | ], 39 | "limit": 10, 40 | "offset": 0, 41 | "total": 2, 42 | } 43 | 44 | # Mock get_chat_workspace_settings 45 | mock_client.get_chat_workspace_settings.return_value = { 46 | "model": "gpt-3.5-turbo", 47 | "indexUids": ["products", "docs"], 48 | "temperature": 0.7, 49 | } 50 | 51 | # Mock update_chat_workspace_settings 52 | mock_client.update_chat_workspace_settings.return_value = { 53 | "model": "gpt-4", 54 | "indexUids": ["products", "docs"], 55 | "temperature": 0.5, 56 | } 57 | 58 | # Replace the chat manager's client 59 | server.chat_manager.client = mock_client 60 | return server 61 | 62 | 63 | async def simulate_tool_call(server, tool_name, arguments=None): 64 | """Simulate a tool call directly on the server""" 65 | handlers = {} 66 | 67 | # Get the tool handler 68 | @server.server.call_tool() 69 | async def handle_call_tool(name, arguments_=None): 70 | pass 71 | 72 | # The handler is registered, now call it directly 73 | result = await server._setup_handlers.__code__.co_consts[1](tool_name, arguments) 74 | 75 | # Actually call the handler through the server's method 76 | handler_func = None 77 | for name, obj in server.__class__.__dict__.items(): 78 | if hasattr(obj, "__name__") and obj.__name__ == "handle_call_tool": 79 | handler_func = obj 80 | break 81 | 82 | # Call the actual handle_call_tool method 83 | return await server._MeilisearchMCPServer__handle_call_tool(tool_name, arguments) 84 | 85 | 86 | class TestChatTools: 87 | @pytest.mark.asyncio 88 | async def test_create_chat_completion(self, setup_mock_chat_client): 89 | """Test creating a chat completion""" 90 | server = setup_mock_chat_client 91 | 92 | # Simulate the tool call 93 | result = await server.chat_manager.create_chat_completion( 94 | workspace_uid="test-workspace", 95 | messages=[ 96 | {"role": "user", "content": "What is Meilisearch?"}, 97 | ], 98 | model="gpt-3.5-turbo", 99 | stream=True, 100 | ) 101 | 102 | # The result should be the combined response 103 | assert result == "This is a test response." 104 | 105 | @pytest.mark.asyncio 106 | async def test_get_chat_workspaces(self, setup_mock_chat_client): 107 | """Test getting chat workspaces""" 108 | server = setup_mock_chat_client 109 | 110 | result = await server.chat_manager.get_chat_workspaces(offset=0, limit=10) 111 | 112 | assert "results" in result 113 | assert len(result["results"]) == 2 114 | assert result["results"][0]["uid"] == "workspace1" 115 | assert result["total"] == 2 116 | 117 | @pytest.mark.asyncio 118 | async def test_get_chat_workspace_settings(self, setup_mock_chat_client): 119 | """Test getting chat workspace settings""" 120 | server = setup_mock_chat_client 121 | 122 | result = await server.chat_manager.get_chat_workspace_settings( 123 | workspace_uid="workspace1" 124 | ) 125 | 126 | assert result["model"] == "gpt-3.5-turbo" 127 | assert "indexUids" in result 128 | assert result["temperature"] == 0.7 129 | 130 | @pytest.mark.asyncio 131 | async def test_update_chat_workspace_settings(self, setup_mock_chat_client): 132 | """Test updating chat workspace settings""" 133 | server = setup_mock_chat_client 134 | 135 | result = await server.chat_manager.update_chat_workspace_settings( 136 | workspace_uid="workspace1", 137 | settings={"model": "gpt-4", "temperature": 0.5}, 138 | ) 139 | 140 | assert result["model"] == "gpt-4" 141 | assert result["temperature"] == 0.5 142 | 143 | @pytest.mark.asyncio 144 | async def test_chat_completion_error_handling(self, server): 145 | """Test error handling in chat completion""" 146 | # Mock the client to raise an error 147 | server.chat_manager.client = MagicMock() 148 | # Create a mock request object for the error with proper JSON text 149 | mock_request = MagicMock() 150 | mock_request.status_code = 400 151 | mock_request.text = '{"message": "Chat feature not enabled", "code": "chat_not_enabled", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#chat_not_enabled"}' 152 | server.chat_manager.client.create_chat_completion.side_effect = ( 153 | MeilisearchApiError("Chat feature not enabled", mock_request) 154 | ) 155 | 156 | with pytest.raises(MeilisearchApiError): 157 | await server.chat_manager.create_chat_completion( 158 | workspace_uid="test", 159 | messages=[{"role": "user", "content": "test"}], 160 | ) 161 | 162 | @pytest.mark.asyncio 163 | async def test_empty_chat_response(self, server): 164 | """Test handling empty chat response""" 165 | # Mock empty response 166 | server.chat_manager.client = MagicMock() 167 | 168 | def mock_empty_completion(*args, **kwargs): 169 | # Return empty chunks 170 | return iter([]) 171 | 172 | server.chat_manager.client.create_chat_completion = mock_empty_completion 173 | 174 | result = await server.chat_manager.create_chat_completion( 175 | workspace_uid="test", 176 | messages=[{"role": "user", "content": "test"}], 177 | ) 178 | 179 | assert result == "" # Empty response should return empty string 180 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## 🚨 IMPORTANT: Development Workflow Guidelines 6 | 7 | **ALL coding agents MUST follow these mandatory guidelines for every task.** 8 | 9 | ### 🔄 Fresh Start Protocol 10 | 11 | **BEFORE starting ANY new task or issue:** 12 | 13 | 1. **Always start from latest main**: 14 | ```bash 15 | git checkout main 16 | git pull origin main # Ensure you have the latest changes 17 | git checkout -b feature/your-branch-name # Create new branch 18 | ``` 19 | 20 | 2. **Verify clean state**: 21 | ```bash 22 | git status # Should show clean working directory 23 | ``` 24 | 25 | 3. **Never carry over unrelated changes** from previous work 26 | 4. **Each task gets its own focused branch** from latest main 27 | 28 | ### 🎯 Focused Development Rules 29 | 30 | **ONLY make changes directly related to the specific task/issue:** 31 | 32 | - ✅ **DO**: Add/modify code that solves the specific issue 33 | - ✅ **DO**: Add focused tests for the specific functionality 34 | - ✅ **DO**: Update documentation if specifically required 35 | - ❌ **DON'T**: Include formatting changes to unrelated files 36 | - ❌ **DON'T**: Add comprehensive test suites unless specifically requested 37 | - ❌ **DON'T**: Refactor unrelated code 38 | - ❌ **DON'T**: Include previous work from other branches 39 | 40 | ### 📋 Task Assessment Phase 41 | 42 | Before writing any code, determine scope: 43 | 44 | 1. **Read the issue/task carefully** - understand exact requirements 45 | 2. **Identify minimal changes needed** - what files need modification? 46 | 3. **Plan focused tests** - only for the specific functionality being added 47 | 4. **Avoid scope creep** - resist urge to "improve" unrelated code 48 | 49 | ### 🧪 Test-Driven Development (TDD) Approach 50 | 51 | When tests are required for the specific task: 52 | 53 | ```bash 54 | # 1. Write failing tests FIRST (focused on the issue) 55 | python -m pytest tests/test_specific_issue.py -v # Should fail 56 | 57 | # 2. Write minimal code to make tests pass 58 | # Edit ONLY files needed for the specific issue 59 | 60 | # 3. Run tests to verify they pass 61 | python -m pytest tests/test_specific_issue.py -v # Should pass 62 | 63 | # 4. Refactor if needed, but stay focused 64 | ``` 65 | 66 | ### 📝 Commit Standards 67 | 68 | **Each commit should be atomic and focused:** 69 | 70 | ```bash 71 | # Format only the files you changed 72 | black src/specific_file.py tests/test_specific_file.py 73 | 74 | # Run tests to ensure no regressions 75 | python -m pytest tests/ -v 76 | 77 | # Commit with descriptive message 78 | git add src/specific_file.py tests/test_specific_file.py 79 | git commit -m "Fix issue #X: Brief description of what was fixed" 80 | ``` 81 | 82 | ### 🚫 What NOT to Include in PRs 83 | 84 | - Formatting changes to files you didn't functionally modify 85 | - Test files not related to your specific task 86 | - Refactoring of unrelated code 87 | - Documentation updates not specifically requested 88 | - Code from previous branches or incomplete work 89 | 90 | ### ✅ PR Quality Checklist 91 | 92 | Before creating PR, verify: 93 | - [ ] Branch created from latest main 94 | - [ ] Only files related to the specific issue are modified 95 | - [ ] Tests pass and are focused on the issue 96 | - [ ] Commit messages are clear and specific 97 | - [ ] No unrelated formatting or code changes 98 | - [ ] PR description clearly links to the issue being solved 99 | 100 | **⚠️ PRs with unrelated changes will be rejected and must be redone.** 101 | 102 | ## Project Overview 103 | 104 | This is a **Model Context Protocol (MCP) server** for Meilisearch, allowing LLM interfaces like Claude to interact with Meilisearch search engines. The project implements a Python-based MCP server that provides comprehensive tools for index management, document operations, search functionality, and system monitoring. 105 | 106 | ## Development Commands 107 | 108 | ### Environment Setup 109 | ```bash 110 | # Create virtual environment and install dependencies 111 | uv venv 112 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 113 | uv pip install -e . 114 | 115 | # Install development dependencies 116 | uv pip install -r requirements-dev.txt 117 | ``` 118 | 119 | ### Testing (MANDATORY for all development) 120 | ```bash 121 | # Run all tests (required before any commit) 122 | python -m pytest tests/ -v 123 | 124 | # Run specific test file 125 | python -m pytest tests/test_mcp_client.py -v 126 | 127 | # Run tests with coverage (required for new features) 128 | python -m pytest --cov=src tests/ 129 | 130 | # Watch mode for development (optional) 131 | pytest-watch tests/ 132 | ``` 133 | 134 | ### Code Quality (MANDATORY before commit) 135 | ```bash 136 | # Format code (required before commit) 137 | black src/ tests/ 138 | 139 | # Check formatting without applying 140 | black --check src/ tests/ 141 | 142 | # Run the MCP server locally for testing 143 | python -m src.meilisearch_mcp 144 | 145 | # Test with MCP Inspector 146 | npx @modelcontextprotocol/inspector python -m src.meilisearch_mcp 147 | ``` 148 | 149 | ### Prerequisites for Testing 150 | - **Meilisearch server** must be running on `http://localhost:7700` 151 | - **Docker option**: `docker run -d -p 7700:7700 getmeili/meilisearch:v1.6` 152 | - **Node.js** for MCP Inspector testing 153 | 154 | ## Architecture 155 | 156 | ### Modular Manager Design 157 | The codebase follows a modular architecture where functionality is organized into specialized managers: 158 | 159 | ``` 160 | MeilisearchClient 161 | ├── IndexManager - Index creation, listing, deletion 162 | ├── DocumentManager - Document CRUD operations 163 | ├── SettingsManager - Index configuration management 164 | ├── TaskManager - Asynchronous task monitoring 165 | ├── KeyManager - API key management 166 | └── MonitoringManager - Health checks and system metrics 167 | ``` 168 | 169 | ### MCP Server Integration 170 | - **Server Class**: `MeilisearchMCPServer` in `server.py` handles MCP protocol communication 171 | - **Tool Registration**: All tools are defined with JSON schemas for input validation 172 | - **Error Handling**: Comprehensive exception handling with logging through `MCPLogger` 173 | - **Dynamic Configuration**: Runtime connection settings updates via MCP tools 174 | 175 | ### Key Components 176 | 177 | #### Tool Handler Pattern 178 | All MCP tools follow a consistent pattern: 179 | 1. Input validation via JSON schema 180 | 2. Delegation to appropriate manager class 181 | 3. Error handling with structured logging 182 | 4. Formatted response as `TextContent` 183 | 184 | #### Search Architecture 185 | - **Single Index Search**: Direct search in specified index 186 | - **Multi-Index Search**: Parallel search across all indices when no `indexUid` provided 187 | - **Result Aggregation**: Smart filtering of results with hits 188 | 189 | #### Connection Management 190 | - **Runtime Configuration**: Dynamic URL and API key updates 191 | - **Environment Variables**: `MEILI_HTTP_ADDR` and `MEILI_MASTER_KEY` for defaults 192 | - **Connection State**: Maintained in server instance for session persistence 193 | 194 | ## Testing Strategy 195 | 196 | ### Test Structure 197 | - **Integration Tests** (`test_mcp_client.py`): End-to-end MCP tool execution with real Meilisearch 198 | - **Unit Tests** (`test_server.py`): Basic server instantiation and configuration 199 | 200 | ### Test Categories by Development Task 201 | 202 | #### When Tests are REQUIRED: 203 | - **New MCP Tools**: Add tests to `test_mcp_client.py` using `simulate_tool_call()` 204 | - **Existing Tool Changes**: Update corresponding test methods 205 | - **Manager Class Changes**: Test through MCP tool integration 206 | - **Bug Fixes**: Add regression tests to prevent reoccurrence 207 | - **API Changes**: Update tests to reflect new interfaces 208 | 209 | #### When Tests are OPTIONAL: 210 | - **Documentation Updates**: README.md, CLAUDE.md changes 211 | - **Code Formatting**: Black formatting, comment changes 212 | - **Minor Refactoring**: Internal reorganization without behavior changes 213 | 214 | ### Tool Simulation Framework 215 | Tests use `simulate_tool_call()` function that: 216 | - Directly invokes server tool handlers 217 | - Bypasses MCP protocol overhead 218 | - Returns proper `TextContent` responses 219 | - Provides comprehensive coverage of all 20+ tools 220 | - Enables fast test execution without MCP protocol complexity 221 | 222 | ### Test Isolation and Best Practices 223 | - **Unique Index Names**: Timestamped index names prevent test interference 224 | - **Cleanup Fixtures**: Automatic test environment cleanup after each test 225 | - **Service Dependencies**: Tests require running Meilisearch instance 226 | - **Test Naming**: Use descriptive test method names (e.g., `test_create_index_with_primary_key`) 227 | - **Assertions**: Test both success cases and error handling 228 | - **Coverage**: New tools must have comprehensive test coverage 229 | - **Embedder Tests**: Tests requiring Meilisearch embedder configuration should be marked with `@pytest.mark.skip` decorator 230 | 231 | ## Environment Configuration 232 | 233 | ### Required Environment Variables 234 | ```bash 235 | MEILI_HTTP_ADDR=http://localhost:7700 # Meilisearch server URL 236 | MEILI_MASTER_KEY=your_master_key # Optional: API key for authenticated instances 237 | ``` 238 | 239 | ### Claude Desktop Integration 240 | Add to `claude_desktop_config.json`: 241 | ```json 242 | { 243 | "mcpServers": { 244 | "meilisearch": { 245 | "command": "uvx", 246 | "args": ["-n", "meilisearch-mcp"] 247 | } 248 | } 249 | } 250 | ``` 251 | 252 | ### GitHub Actions Integration 253 | The repository includes Claude Code integration via GitHub Actions: 254 | - **Trigger**: Comments containing `@claude` on issues, PRs, or reviews 255 | - **Workflow**: `.github/workflows/claude.yml` handles automated Claude responses 256 | - **Permissions**: Read contents, write to pull requests and issues 257 | 258 | ## Available MCP Tools 259 | 260 | ### Core Categories 261 | - **Connection Management**: Dynamic configuration updates 262 | - **Index Operations**: CRUD operations for search indices 263 | - **Document Management**: Add, retrieve, and manage documents 264 | - **Search Capabilities**: Single and multi-index search with filtering 265 | - **Settings Control**: Index configuration and optimization 266 | - **Task Monitoring**: Asynchronous operation tracking 267 | - **API Key Management**: Authentication and authorization 268 | - **System Monitoring**: Health checks and performance metrics 269 | 270 | ### Search Tool Features 271 | - **Flexible Targeting**: Search specific index or all indices 272 | - **Rich Parameters**: Filtering, sorting, pagination support 273 | - **Hybrid Search**: Support for combining keyword and semantic search with `semanticRatio` parameter 274 | - **Vector Search**: Custom vector support for semantic similarity search 275 | - **Result Formatting**: JSON formatted responses with proper serialization 276 | - **Error Resilience**: Graceful handling of index-specific failures 277 | 278 | ## Development Notes 279 | 280 | ### Dependencies 281 | - **MCP Framework**: `mcp>=1.2.1` for protocol implementation 282 | - **Meilisearch Client**: `meilisearch>=0.34.0` for search engine integration with stable AI-powered search features 283 | - **HTTP Client**: `httpx>=0.24.0` for async HTTP operations 284 | - **Data Validation**: `pydantic>=2.0.0` for structured data handling 285 | 286 | ### Logging Infrastructure 287 | - **Structured Logging**: JSON-formatted logs with contextual information 288 | - **Log Directory**: `~/.meilisearch-mcp/logs/` for persistent logging 289 | - **Error Tracking**: Comprehensive error logging with tool context 290 | 291 | ### Hybrid Search Implementation 292 | - **Dependency**: Requires `meilisearch>=0.34.0` for stable AI-powered search features 293 | - **Parameters**: `hybrid` object with `semanticRatio` (0.0-1.0) and `embedder` (required) 294 | - **Vector Support**: Custom vectors can be provided via `vector` parameter 295 | - **Testing**: Hybrid search tests require embedder configuration in Meilisearch 296 | - **Backward Compatibility**: All hybrid search parameters are optional to maintain compatibility -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
⚡ Connect any LLM to Meilisearch and supercharge your AI with lightning-fast search capabilities! 🔍
23 | 24 | ## 🤔 What is this? 25 | 26 | The Meilisearch MCP Server is a Model Context Protocol server that enables any MCP-compatible client (including Claude, OpenAI agents, and other LLMs) to interact with Meilisearch. This stdio-based server allows AI assistants to manage search indices, perform searches, and handle your data through natural conversation. 27 | 28 | **Why use this?** 29 | - 🤖 **Universal Compatibility** - Works with any MCP client, not just Claude 30 | - 🗣️ **Natural Language Control** - Manage Meilisearch through conversation with any LLM 31 | - 🚀 **Zero Learning Curve** - No need to learn Meilisearch's API 32 | - 🔧 **Full Feature Access** - All Meilisearch capabilities at your fingertips 33 | - 🔄 **Dynamic Connections** - Switch between Meilisearch instances on the fly 34 | - 📡 **stdio Transport** - Currently uses stdio; native Meilisearch MCP support coming soon! 35 | 36 | ## ✨ Key Features 37 | 38 | - 📊 **Index & Document Management** - Create, update, and manage search indices 39 | - 🔍 **Smart Search** - Search across single or multiple indices with advanced filtering 40 | - ⚙️ **Settings Configuration** - Fine-tune search relevancy and performance 41 | - 📈 **Task Monitoring** - Track indexing progress and system operations 42 | - 🔐 **API Key Management** - Secure access control 43 | - 🏥 **Health Monitoring** - Keep tabs on your Meilisearch instance 44 | - 🐍 **Python Implementation** - [TypeScript version also available](https://github.com/devlimelabs/meilisearch-ts-mcp) 45 | 46 | ## 🚀 Quick Start 47 | 48 | Get up and running in just 3 steps! 49 | 50 | ### 1️⃣ Install the package 51 | 52 | ```bash 53 | # Using pip 54 | pip install meilisearch-mcp 55 | 56 | # Or using uvx (recommended) 57 | uvx -n meilisearch-mcp 58 | ``` 59 | 60 | ### 2️⃣ Configure Claude Desktop 61 | 62 | Add this to your `claude_desktop_config.json`: 63 | 64 | ```json 65 | { 66 | "mcpServers": { 67 | "meilisearch": { 68 | "command": "uvx", 69 | "args": ["-n", "meilisearch-mcp"] 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | ### 3️⃣ Start Meilisearch 76 | 77 | ```bash 78 | # Using Docker (recommended) 79 | docker run -d -p 7700:7700 getmeili/meilisearch:v1.6 80 | 81 | # Or using Homebrew 82 | brew install meilisearch 83 | meilisearch 84 | ``` 85 | 86 | That's it! Now you can ask your AI assistant to search and manage your Meilisearch data! 🎉 87 | 88 | ## 📚 Examples 89 | 90 | ### 💬 Talk to your AI assistant naturally: 91 | 92 | ``` 93 | You: "Create a new index called 'products' with 'id' as the primary key" 94 | AI: I'll create that index for you... ✓ Index 'products' created successfully! 95 | 96 | You: "Add some products to the index" 97 | AI: I'll add those products... ✓ Added 5 documents to 'products' index 98 | 99 | You: "Search for products under $50 with 'electronics' in the category" 100 | AI: I'll search for those products... Found 12 matching products! 101 | ``` 102 | 103 | ### 🔍 Advanced Search Example: 104 | 105 | ``` 106 | You: "Search across all my indices for 'machine learning' and sort by date" 107 | AI: Searching across all indices... Found 47 results from 3 indices: 108 | - 'blog_posts': 23 articles about ML 109 | - 'documentation': 15 technical guides 110 | - 'tutorials': 9 hands-on tutorials 111 | ``` 112 | 113 | ## 🔧 Installation 114 | 115 | ### Prerequisites 116 | 117 | - Python ≥ 3.9 118 | - Running Meilisearch instance 119 | - MCP-compatible client (Claude Desktop, OpenAI agents, etc.) 120 | 121 | ### From PyPI 122 | 123 | ```bash 124 | pip install meilisearch-mcp 125 | ``` 126 | 127 | ### From Source (for development) 128 | 129 | ```bash 130 | # Clone repository 131 | git clone https://github.com/meilisearch/meilisearch-mcp.git 132 | cd meilisearch-mcp 133 | 134 | # Create virtual environment and install 135 | uv venv 136 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 137 | uv pip install -e . 138 | ``` 139 | 140 | ### Using Docker 141 | 142 | Perfect for containerized environments like n8n workflows! 143 | 144 | #### From Docker Hub 145 | 146 | ```bash 147 | # Pull the latest image 148 | docker pull getmeili/meilisearch-mcp:latest 149 | 150 | # Or a specific version 151 | docker pull getmeili/meilisearch-mcp:0.5.0 152 | 153 | # Run the container 154 | docker run -it \ 155 | -e MEILI_HTTP_ADDR=http://your-meilisearch:7700 \ 156 | -e MEILI_MASTER_KEY=your-master-key \ 157 | getmeili/meilisearch-mcp:latest 158 | ``` 159 | 160 | #### Build from Source 161 | 162 | ```bash 163 | # Build your own image 164 | docker build -t meilisearch-mcp . 165 | docker run -it \ 166 | -e MEILI_HTTP_ADDR=http://your-meilisearch:7700 \ 167 | -e MEILI_MASTER_KEY=your-master-key \ 168 | meilisearch-mcp 169 | ``` 170 | 171 | #### Integration with n8n 172 | 173 | For n8n workflows, you can use the Docker image directly in your setup: 174 | ```yaml 175 | meilisearch-mcp: 176 | image: getmeili/meilisearch-mcp:latest 177 | environment: 178 | - MEILI_HTTP_ADDR=http://meilisearch:7700 179 | - MEILI_MASTER_KEY=masterKey 180 | ``` 181 | 182 | ## 🛠️ What Can You Do? 183 | 184 |
329 | Meilisearch is an open-source search engine that offers a delightful search experience.
330 | Learn more about Meilisearch at meilisearch.com
331 |