This is page 1 of 2. Use http://codebase.md/devlimelabs/meilisearch-ts-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .node-loader.mjs ├── CONTRIBUTING.md ├── directory-structure.md ├── docker-compose.yml ├── Dockerfile ├── examples │ └── movies-demo.js ├── implementation-plan.md ├── jest.config.js ├── LICENSE ├── meilisearch.open-api.json ├── package-lock.json ├── package.json ├── README.md ├── scripts │ ├── claude-desktop-setup.js │ └── setup-dev.sh ├── smithery.yaml ├── src │ ├── __tests__ │ │ └── api-client.test.ts │ ├── config.ts │ ├── index.ts │ ├── tools │ │ ├── document-tools.ts │ │ ├── index-tools.ts │ │ ├── search-tools.ts │ │ ├── settings-tools.ts │ │ ├── system-tools.ts │ │ ├── task-tools.ts │ │ └── vector-tools.ts │ ├── types │ │ └── global.d.ts │ └── utils │ ├── api-client.ts │ └── error-handler.ts ├── tsconfig.json └── vector-search-guide.md ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Meilisearch MCP Server Environment Variables 2 | 3 | # URL of the Meilisearch instance 4 | MEILISEARCH_HOST=http://localhost:7700 5 | # API key for authenticating with Meilisearch 6 | # Leave empty if no key is required for your Meilisearch instance 7 | MEILISEARCH_API_KEY= 8 | 9 | # Timeout for API requests in milliseconds 10 | MEILISEARCH_TIMEOUT=5000 11 | ``` -------------------------------------------------------------------------------- /.node-loader.mjs: -------------------------------------------------------------------------------- ``` 1 | export const resolve = (specifier, context, nextResolve) => { 2 | return nextResolve(specifier, context); 3 | }; 4 | 5 | export const load = (url, context, nextLoad) => { 6 | return nextLoad(url, context); 7 | }; 8 | 9 | export const getFormat = (url, context, defaultGetFormat) => { 10 | if (url.endsWith('.ts')) { 11 | return { format: 'module' }; 12 | } 13 | return defaultGetFormat(url, context); 14 | }; 15 | ``` -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "env": { 3 | "es2022": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "indent": ["error", 2], 20 | "linebreak-style": ["error", "unix"], 21 | "quotes": ["error", "single", { "avoidEscape": true }], 22 | "semi": ["error", "always"], 23 | "@typescript-eslint/no-explicit-any": "warn", 24 | "@typescript-eslint/explicit-function-return-type": "off", 25 | "@typescript-eslint/explicit-module-boundary-types": "off" 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Meilisearch MCP Server 2 | 3 | [](https://smithery.ai/server/@devlimelabs/meilisearch-ts-mcp) 4 | 5 | A Model Context Protocol (MCP) server implementation for Meilisearch, enabling AI assistants to interact with Meilisearch through a standardized interface. 6 | 7 | ## Features 8 | 9 | - **Index Management**: Create, update, and delete indexes 10 | - **Document Management**: Add, update, and delete documents 11 | - **Search Capabilities**: Perform searches with various parameters and filters 12 | - **Settings Management**: Configure index settings 13 | - **Task Management**: Monitor and manage asynchronous tasks 14 | - **System Operations**: Health checks, version information, and statistics 15 | - **Vector Search**: Experimental vector search capabilities 16 | 17 | ## Installation 18 | 19 | ### Installing via Smithery 20 | 21 | To install Meilisearch MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@devlimelabs/meilisearch-ts-mcp): 22 | 23 | ```bash 24 | npx -y @smithery/cli install @devlimelabs/meilisearch-ts-mcp --client claude 25 | ``` 26 | 27 | ### Manual Installation 28 | 1. Clone the repository: 29 | ```bash 30 | git clone https://github.com/devlimelabs/meilisearch-ts-mcp.git 31 | cd meilisearch-ts-mcp 32 | ``` 33 | 34 | 2. Install dependencies: 35 | ```bash 36 | npm install 37 | ``` 38 | 39 | 3. Create a `.env` file based on the example: 40 | ```bash 41 | cp .env.example .env 42 | ``` 43 | 44 | 4. Edit the `.env` file to configure your Meilisearch connection. 45 | 46 | ## Docker Setup 47 | 48 | The Meilisearch MCP Server can be run in a Docker container for easier deployment and isolation. 49 | 50 | ### Using Docker Compose 51 | 52 | The easiest way to get started with Docker is to use Docker Compose: 53 | 54 | ```bash 55 | # Start the Meilisearch MCP Server 56 | docker-compose up -d 57 | 58 | # View logs 59 | docker-compose logs -f 60 | 61 | # Stop the server 62 | docker-compose down 63 | ``` 64 | 65 | ### Building and Running the Docker Image Manually 66 | 67 | You can also build and run the Docker image manually: 68 | 69 | ```bash 70 | # Build the Docker image 71 | docker build -t meilisearch-ts-mcp . 72 | 73 | # Run the container 74 | docker run -p 3000:3000 --env-file .env meilisearch-ts-mcp 75 | ``` 76 | 77 | ## Development Setup 78 | 79 | For developers who want to contribute to the Meilisearch MCP Server, we provide a convenient setup script: 80 | 81 | ```bash 82 | # Clone the repository 83 | git clone https://github.com/devlimelabs-ts-mcp/meilisearch-ts-mcp.git 84 | cd meilisearch-ts-mcp 85 | 86 | # Run the development setup script 87 | ./scripts/setup-dev.sh 88 | ``` 89 | 90 | The setup script will: 91 | 1. Create a `.env` file from `.env.example` if it doesn't exist 92 | 2. Install dependencies 93 | 3. Build the project 94 | 4. Run tests to ensure everything is working correctly 95 | 96 | After running the setup script, you can start the server in development mode: 97 | 98 | ```bash 99 | npm run dev 100 | ``` 101 | 102 | ## Usage 103 | 104 | ### Building the Project 105 | 106 | ```bash 107 | npm run build 108 | ``` 109 | 110 | ### Running the Server 111 | 112 | ```bash 113 | npm start 114 | ``` 115 | 116 | ### Development Mode 117 | 118 | ```bash 119 | npm run dev 120 | ``` 121 | 122 | ## Claude Desktop Integration 123 | 124 | The Meilisearch MCP Server can be integrated with Claude for Desktop, allowing you to interact with your Meilisearch instance directly through Claude. 125 | 126 | ### Automated Setup 127 | 128 | We provide a setup script that automatically configures Claude for Desktop to work with the Meilisearch MCP Server: 129 | 130 | ```bash 131 | # First build the project 132 | npm run build 133 | 134 | # Then run the setup script 135 | node scripts/claude-desktop-setup.js 136 | ``` 137 | 138 | The script will: 139 | 1. Detect your operating system and locate the Claude for Desktop configuration file 140 | 2. Read your Meilisearch configuration from the `.env` file 141 | 3. Generate the necessary configuration for Claude for Desktop 142 | 4. Provide instructions for updating your Claude for Desktop configuration 143 | 144 | ### Manual Setup 145 | 146 | If you prefer to manually configure Claude for Desktop: 147 | 148 | 1. Locate your Claude for Desktop configuration file: 149 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 150 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 151 | - **Linux**: `~/.config/Claude/claude_desktop_config.json` 152 | 153 | 2. Add the following configuration (adjust paths as needed): 154 | 155 | ```json 156 | { 157 | "mcpServers": { 158 | "meilisearch": { 159 | "command": "node", 160 | "args": ["/path/to/meilisearch-ts-mcp/dist/index.js"], 161 | "env": { 162 | "MEILISEARCH_HOST": "http://localhost:7700", 163 | "MEILISEARCH_API_KEY": "your-api-key" 164 | } 165 | } 166 | } 167 | } 168 | ``` 169 | 170 | 3. Restart Claude for Desktop to apply the changes. 171 | 172 | 4. In Claude, type: "I want to use the Meilisearch MCP server" to activate the integration. 173 | 174 | ## Cursor Integration 175 | 176 | The Meilisearch MCP Server can also be integrated with [Cursor](https://cursor.com), an AI-powered code editor. 177 | 178 | ### Setting Up MCP in Cursor 179 | 180 | 1. Install and set up the Meilisearch MCP Server: 181 | ```bash 182 | git clone https://github.com/devlimelabs/meilisearch-ts-mcp.git 183 | cd meilisearch-ts-mcp 184 | npm install 185 | npm run build 186 | ``` 187 | 188 | 2. Start the MCP server: 189 | ```bash 190 | npm start 191 | ``` 192 | 193 | 3. In Cursor, open the Command Palette (Cmd/Ctrl+Shift+P) and search for "MCP: Connect to MCP Server". 194 | 195 | 4. Select "Connect to a local MCP server" and enter the following details: 196 | - **Name**: Meilisearch 197 | - **Command**: node 198 | - **Arguments**: /absolute/path/to/meilisearch-ts-mcp/dist/index.js 199 | - **Environment Variables**: 200 | ``` 201 | MEILISEARCH_HOST=http://localhost:7700 202 | MEILISEARCH_API_KEY=your-api-key 203 | ``` 204 | 205 | 5. Click "Connect" to establish the connection. 206 | 207 | 6. You can now interact with your Meilisearch instance through Cursor by typing commands like "Search my Meilisearch index for documents about..." 208 | 209 | ## Available Tools 210 | 211 | The Meilisearch MCP Server provides the following tools: 212 | 213 | ### Index Tools 214 | - `create-index`: Create a new index 215 | - `get-index`: Get information about an index 216 | - `list-indexes`: List all indexes 217 | - `update-index`: Update an index 218 | - `delete-index`: Delete an index 219 | 220 | ### Document Tools 221 | - `add-documents`: Add documents to an index 222 | - `get-document`: Get a document by ID 223 | - `get-documents`: Get multiple documents 224 | - `update-documents`: Update documents 225 | - `delete-document`: Delete a document by ID 226 | - `delete-documents`: Delete multiple documents 227 | - `delete-all-documents`: Delete all documents in an index 228 | 229 | ### Search Tools 230 | - `search`: Search for documents 231 | - `multi-search`: Perform multiple searches in a single request 232 | 233 | ### Settings Tools 234 | - `get-settings`: Get index settings 235 | - `update-settings`: Update index settings 236 | - `reset-settings`: Reset index settings to default 237 | - Various specific settings tools (synonyms, stop words, ranking rules, etc.) 238 | 239 | ### Task Tools 240 | - `list-tasks`: List tasks with optional filtering 241 | - `get-task`: Get information about a specific task 242 | - `cancel-tasks`: Cancel tasks based on provided filters 243 | - `wait-for-task`: Wait for a specific task to complete 244 | 245 | ### System Tools 246 | - `health`: Check the health status of the Meilisearch server 247 | - `version`: Get version information 248 | - `info`: Get system information 249 | - `stats`: Get statistics about indexes 250 | 251 | ### Vector Tools (Experimental) 252 | - `enable-vector-search`: Enable vector search 253 | - `get-experimental-features`: Get experimental features status 254 | - `update-embedders`: Configure embedders 255 | - `get-embedders`: Get embedders configuration 256 | - `reset-embedders`: Reset embedders configuration 257 | - `vector-search`: Perform vector search 258 | 259 | ## License 260 | 261 | MIT 262 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to Meilisearch MCP Server 2 | 3 | Thank you for your interest in contributing to the Meilisearch MCP Server! This document provides guidelines and instructions for contributing. 4 | 5 | ## Code of Conduct 6 | 7 | Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community. 8 | 9 | ## Getting Started 10 | 11 | 1. Fork the repository 12 | 2. Clone your fork: `git clone https://github.com/devlimelabs/meilisearch-ts-mcp.git` 13 | 3. Navigate to the project directory: `cd meilisearch-ts-mcp` 14 | 4. Install dependencies: `npm install` 15 | 5. Create a new branch for your feature or bugfix: `git checkout -b feature/your-feature-name` 16 | 17 | ## Development Workflow 18 | 19 | 1. Make your changes 20 | 2. Run the linter: `npm run lint` 21 | 3. Run tests: `npm test` 22 | 4. Build the project: `npm run build` 23 | 5. Test your changes with a local Meilisearch instance 24 | 25 | ## Pull Request Process 26 | 27 | 1. Ensure your code passes all tests and linting 28 | 2. Update documentation if necessary 29 | 3. Submit a pull request to the `main` branch 30 | 4. Describe your changes in detail in the pull request description 31 | 5. Reference any related issues 32 | 33 | ## Adding New Tools 34 | 35 | When adding new tools to the MCP server: 36 | 37 | 1. Create a new file in the `src/tools` directory if appropriate 38 | 2. Follow the existing pattern for tool registration 39 | 3. Use Zod for parameter validation 40 | 4. Add proper error handling 41 | 5. Update the README.md to document the new tool 42 | 43 | ## Coding Standards 44 | 45 | - Use TypeScript for all new code 46 | - Follow the existing code style 47 | - Write meaningful commit messages 48 | - Add comments for complex logic 49 | - Write tests for new functionality 50 | 51 | ## Testing 52 | 53 | - Write unit tests for new functionality 54 | - Ensure all tests pass before submitting a pull request 55 | - Test with a real Meilisearch instance when possible 56 | 57 | ## Documentation 58 | 59 | - Update the README.md file with any new features or changes 60 | - Document all new tools and parameters 61 | - Provide examples for complex functionality 62 | 63 | ## License 64 | 65 | By contributing to this project, you agree that your contributions will be licensed under the project's MIT license. 66 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | extensionsToTreatAsEsm: ['.ts'], 6 | moduleNameMapper: { 7 | '^(\\.{1,2}/.*)\\.js$': '$1', 8 | }, 9 | transform: { 10 | '^.+\\.tsx?$': [ 11 | 'ts-jest', 12 | { 13 | useESM: true, 14 | }, 15 | ], 16 | }, 17 | }; 18 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "resolveJsonModule": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"], 17 | "ts-node": { 18 | "esm": true, 19 | "experimentalSpecifierResolution": "node" 20 | } 21 | } 22 | ``` -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Global type declarations for external modules 3 | */ 4 | 5 | declare module '@modelcontextprotocol/sdk/server/mcp.js' { 6 | export class McpServer { 7 | constructor(options?: { name?: string; version?: string }); 8 | 9 | tool( 10 | name: string, 11 | description: string, 12 | parameters: Record<string, any>, 13 | handler: (args: any, extra: any) => any | Promise<any> 14 | ): void; 15 | 16 | connect(transport: any): Promise<void>; 17 | } 18 | } 19 | 20 | declare module '@modelcontextprotocol/sdk/server/stdio.js' { 21 | export class StdioServerTransport { 22 | constructor(); 23 | } 24 | } 25 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM node:20-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files and install dependencies 6 | COPY package*.json ./ 7 | RUN npm ci 8 | 9 | # Copy source code 10 | COPY . . 11 | 12 | # Build the application 13 | RUN npm run build 14 | 15 | # Production stage 16 | FROM node:20-alpine 17 | 18 | WORKDIR /app 19 | 20 | # Copy package files and install production dependencies only 21 | COPY package*.json ./ 22 | RUN npm ci --omit=dev 23 | 24 | # Copy built application from builder stage 25 | COPY --from=builder /app/dist ./dist 26 | 27 | # Copy .env.example file 28 | COPY .env.example .env.example 29 | 30 | # Set environment variables 31 | ENV NODE_ENV=production 32 | 33 | # Expose port if needed (for health checks, etc.) 34 | # EXPOSE 8080 35 | 36 | # Start the application 37 | CMD ["node", "dist/index.js"] 38 | ``` -------------------------------------------------------------------------------- /scripts/setup-dev.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Setup Development Environment for Meilisearch MCP Server 4 | 5 | # Create .env file if it doesn't exist 6 | if [ ! -f .env ]; then 7 | echo "Creating .env file from .env.example..." 8 | cp .env.example .env 9 | echo "Done! Please edit .env file with your Meilisearch configuration." 10 | else 11 | echo ".env file already exists." 12 | fi 13 | 14 | # Install dependencies 15 | echo "Installing dependencies..." 16 | npm install 17 | 18 | # Build the project 19 | echo "Building the project..." 20 | npm run build 21 | 22 | # Run tests 23 | echo "Running tests..." 24 | if npm test; then 25 | echo "All tests passed!" 26 | else 27 | echo "Warning: Some tests failed. You may need to fix them before proceeding." 28 | echo "You can continue with development, but be aware that some functionality may not work as expected." 29 | fi 30 | 31 | echo "Development environment setup complete!" 32 | echo "To start the server in development mode, run: npm run dev" 33 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | meilisearch: 5 | image: getmeili/meilisearch:latest 6 | container_name: meilisearch 7 | environment: 8 | - MEILI_MASTER_KEY=${MEILI_MASTER_KEY:-masterKey} 9 | - MEILI_NO_ANALYTICS=true 10 | - MEILI_ENV=development 11 | ports: 12 | - '7700:7700' 13 | volumes: 14 | - meilisearch_data:/meili_data 15 | restart: unless-stopped 16 | networks: 17 | - meilisearch-network 18 | 19 | meilisearch-ts-mcp: 20 | build: 21 | context: . 22 | dockerfile: Dockerfile 23 | container_name: meilisearch-ts-mcp 24 | environment: 25 | - MEILISEARCH_HOST=http://meilisearch:7700 26 | - MEILISEARCH_API_KEY=${MEILI_MASTER_KEY:-masterKey} 27 | - MEILISEARCH_TIMEOUT=5000 28 | depends_on: 29 | - meilisearch 30 | networks: 31 | - meilisearch-network 32 | 33 | volumes: 34 | meilisearch_data: 35 | driver: local 36 | 37 | networks: 38 | meilisearch-network: 39 | driver: bridge 40 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | properties: 9 | MEILISEARCH_HOST: 10 | type: string 11 | default: http://localhost:7700 12 | description: URL for the Meilisearch instance 13 | MEILISEARCH_API_KEY: 14 | type: string 15 | default: "" 16 | description: API key for Meilisearch, if required 17 | commandFunction: 18 | # A function that produces the CLI command to start the MCP on stdio. 19 | |- 20 | (config) => ({ 21 | command: 'node', 22 | args: ['dist/index.js'], 23 | env: { 24 | NODE_ENV: 'production', 25 | MEILISEARCH_HOST: config.MEILISEARCH_HOST, 26 | MEILISEARCH_API_KEY: config.MEILISEARCH_API_KEY 27 | } 28 | }) 29 | exampleConfig: 30 | MEILISEARCH_HOST: http://localhost:7700 31 | MEILISEARCH_API_KEY: your-api-key 32 | ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Meilisearch MCP Server Configuration 3 | * 4 | * This file contains the configuration settings for connecting to the Meilisearch server. 5 | * Configuration is loaded from environment variables with sensible defaults. 6 | */ 7 | 8 | // Server configuration interface 9 | export interface ServerConfig { 10 | /** The URL of the Meilisearch instance */ 11 | host: string; 12 | /** The API key for authenticating with Meilisearch */ 13 | apiKey: string; 14 | /** The timeout for API requests in milliseconds */ 15 | timeout: number; 16 | } 17 | 18 | /** 19 | * Load and initialize configuration from environment variables 20 | */ 21 | export const loadConfig = (): ServerConfig => { 22 | return { 23 | host: process.env.MEILISEARCH_HOST || "http://localhost:7700", 24 | apiKey: process.env.MEILISEARCH_API_KEY || "", 25 | timeout: parseInt(process.env.MEILISEARCH_TIMEOUT || "5000", 10), 26 | }; 27 | }; 28 | 29 | // Export the config instance 30 | export const config = loadConfig(); 31 | 32 | // Re-export for direct use 33 | export default config; 34 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "meilisearch-ts-mcp", 3 | "version": "0.1.0", 4 | "description": "Meilisearch MCP Server (Typescript) - Model Context Protocol implementation for Meilisearch", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "dev": "tsx src/index.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "test": "jest" 13 | }, 14 | "keywords": [ 15 | "meilisearch", 16 | "mcp", 17 | "search", 18 | "model-context-protocol" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "dependencies": { 23 | "@modelcontextprotocol/sdk": "^1.6.0", 24 | "axios": "^1.6.2", 25 | "dotenv": "^16.3.1", 26 | "zod": "^3.22.4" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "^29.5.10", 30 | "@types/node": "^20.10.0", 31 | "@typescript-eslint/eslint-plugin": "^6.12.0", 32 | "@typescript-eslint/parser": "^6.12.0", 33 | "eslint": "^8.54.0", 34 | "jest": "^29.7.0", 35 | "ts-jest": "^29.1.1", 36 | "ts-node": "^10.9.1", 37 | "tsx": "^4.19.3", 38 | "typescript": "^5.3.2" 39 | } 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/utils/error-handler.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Error handling utilities for Meilisearch API responses 3 | */ 4 | 5 | 6 | /** 7 | * Formats Meilisearch API errors for consistent error messaging 8 | * 9 | * @param error - The error from the API request 10 | * @returns A formatted error message 11 | */ 12 | export const handleApiError = (error: any): string => { 13 | // If it's an Axios error with a response 14 | if (error.isAxiosError && error.response) { 15 | const { status, data } = error.response; 16 | // Return formatted error with status code and response data 17 | return `Meilisearch API error (${status}): ${JSON.stringify(data)}`; 18 | } 19 | 20 | // If it's a network error or other error 21 | return `Error connecting to Meilisearch: ${error.message}`; 22 | }; 23 | 24 | /** 25 | * Creates a standardized error response object for MCP tools 26 | * 27 | * @param error - The error from the API request 28 | * @returns An MCP tool response object with error flag 29 | */ 30 | export const createErrorResponse = (error: any) => { 31 | return { 32 | isError: true, 33 | content: [{ type: "text", text: handleApiError(error) }], 34 | }; 35 | }; 36 | 37 | export default { 38 | handleApiError, 39 | createErrorResponse, 40 | }; 41 | ``` -------------------------------------------------------------------------------- /src/utils/api-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | 3 | import config from '../config.js'; 4 | 5 | /** 6 | * Meilisearch API client 7 | * 8 | * This module provides a configured Axios instance for making requests to the Meilisearch API. 9 | */ 10 | 11 | /** 12 | * Creates a configured Axios instance for Meilisearch API requests 13 | * 14 | * @returns An Axios instance with base configuration 15 | */ 16 | export const createApiClient = () => { 17 | const instance = axios.create({ 18 | baseURL: config.host, 19 | headers: { 20 | Authorization: `Bearer ${config.apiKey}`, 21 | 'Content-Type': 'application/json', 22 | }, 23 | timeout: config.timeout, 24 | }); 25 | 26 | return { 27 | get: <T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> => 28 | instance.get(url, config), 29 | post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> => 30 | instance.post(url, data, config), 31 | put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> => 32 | instance.put(url, data, config), 33 | patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> => 34 | instance.patch(url, data, config), 35 | delete: <T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> => 36 | instance.delete(url, config), 37 | }; 38 | }; 39 | 40 | // Create and export a singleton instance of the API client 41 | export const apiClient = createApiClient(); 42 | 43 | // Re-export for direct use 44 | export default apiClient; 45 | ``` -------------------------------------------------------------------------------- /src/__tests__/api-client.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * API Client Tests 3 | * 4 | * This file contains tests for the API client utility. 5 | */ 6 | 7 | 8 | // Mock the API client 9 | jest.mock('../utils/api-client', () => { 10 | const mockGet = jest.fn(); 11 | const mockPost = jest.fn(); 12 | const mockPut = jest.fn(); 13 | const mockPatch = jest.fn(); 14 | const mockDelete = jest.fn(); 15 | 16 | return { 17 | createApiClient: jest.fn(() => ({ 18 | get: mockGet, 19 | post: mockPost, 20 | put: mockPut, 21 | patch: mockPatch, 22 | delete: mockDelete 23 | })), 24 | apiClient: { 25 | get: mockGet, 26 | post: mockPost, 27 | put: mockPut, 28 | patch: mockPatch, 29 | delete: mockDelete 30 | }, 31 | __esModule: true, 32 | default: { 33 | get: mockGet, 34 | post: mockPost, 35 | put: mockPut, 36 | patch: mockPatch, 37 | delete: mockDelete 38 | } 39 | }; 40 | }); 41 | 42 | // Get the mocked functions 43 | const { apiClient } = require('../utils/api-client'); 44 | const mockGet = apiClient.get; 45 | const mockPost = apiClient.post; 46 | 47 | describe('API Client', () => { 48 | beforeEach(() => { 49 | jest.clearAllMocks(); 50 | }); 51 | 52 | it('should make GET requests correctly', async () => { 53 | // Setup 54 | mockGet.mockResolvedValueOnce({ data: { result: 'success' } }); 55 | 56 | // Execute 57 | await apiClient.get('/test-endpoint'); 58 | 59 | // Verify 60 | expect(mockGet).toHaveBeenCalledWith('/test-endpoint'); 61 | }); 62 | 63 | it('should include configuration when provided', async () => { 64 | // Setup 65 | mockGet.mockResolvedValueOnce({ data: { result: 'success' } }); 66 | const config = { params: { filter: 'test' } }; 67 | 68 | // Execute 69 | await apiClient.get('/test-endpoint', config); 70 | 71 | // Verify 72 | expect(mockGet).toHaveBeenCalledWith('/test-endpoint', config); 73 | }); 74 | 75 | it('should handle errors appropriately', async () => { 76 | // Setup 77 | const errorResponse = { 78 | response: { 79 | status: 404, 80 | data: { message: 'Not found' } 81 | } 82 | }; 83 | mockGet.mockRejectedValueOnce(errorResponse); 84 | 85 | // Execute & Verify 86 | await expect(apiClient.get('/non-existent')).rejects.toEqual(errorResponse); 87 | }); 88 | }); 89 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import axios from 'axios'; 4 | 5 | import registerDocumentTools from './tools/document-tools.js'; 6 | import registerIndexTools from './tools/index-tools.js'; 7 | import registerSearchTools from './tools/search-tools.js'; 8 | import registerSettingsTools from './tools/settings-tools.js'; 9 | import registerSystemTools from './tools/system-tools.js'; 10 | import registerTaskTools from './tools/task-tools.js'; 11 | import registerVectorTools from './tools/vector-tools.js'; 12 | 13 | /** 14 | * Meilisearch MCP Server 15 | * 16 | * This is the main entry point for the Meilisearch MCP server. 17 | * It integrates with Meilisearch to provide search capabilities to LLMs via the Model Context Protocol. 18 | */ 19 | 20 | // Import tool registration functions 21 | // Server configuration 22 | interface ServerConfig { 23 | host: string; 24 | apiKey: string; 25 | } 26 | 27 | // Initialize configuration from environment variables 28 | const config: ServerConfig = { 29 | host: process.env.MEILISEARCH_HOST || "http://localhost:7700", 30 | apiKey: process.env.MEILISEARCH_API_KEY || "", 31 | }; 32 | 33 | // Create Axios instance with base configuration 34 | const api = axios.create({ 35 | baseURL: config.host, 36 | headers: { 37 | Authorization: `Bearer ${config.apiKey}`, 38 | "Content-Type": "application/json", 39 | }, 40 | }); 41 | 42 | // Helper function to handle API errors 43 | const handleApiError = (error: any): string => { 44 | if (error.response) { 45 | const { status, data } = error.response; 46 | return `Meilisearch API error (${status}): ${JSON.stringify(data)}`; 47 | } 48 | return `Error connecting to Meilisearch: ${error.message}`; 49 | }; 50 | 51 | /** 52 | * Main function to initialize and start the MCP server 53 | */ 54 | async function main() { 55 | console.error("Starting Meilisearch MCP Server..."); 56 | 57 | // Create the MCP server instance 58 | const server = new McpServer({ 59 | name: "meilisearch", 60 | version: "1.0.0", 61 | }); 62 | 63 | // Register all tools 64 | registerIndexTools(server); 65 | registerDocumentTools(server); 66 | registerSearchTools(server); 67 | registerSettingsTools(server); 68 | registerVectorTools(server); 69 | registerSystemTools(server); 70 | registerTaskTools(server); 71 | 72 | // Connect to the stdio transport 73 | const transport = new StdioServerTransport(); 74 | await server.connect(transport); 75 | 76 | console.error("Meilisearch MCP Server is running on stdio transport"); 77 | } 78 | 79 | // Start the server 80 | main().catch(error => { 81 | console.error("Fatal error:", error); 82 | process.exit(1); 83 | }); 84 | ``` -------------------------------------------------------------------------------- /implementation-plan.md: -------------------------------------------------------------------------------- ```markdown 1 | # Meilisearch MCP Server Implementation Plan 2 | 3 | ## Overview 4 | This document outlines the implementation plan for the Meilisearch MCP Server, which integrates with Meilisearch to provide search capabilities to LLMs via the Model Context Protocol (MCP). 5 | 6 | ## Project Structure 7 | 8 | ``` 9 | meilisearch-ts-mcp/ 10 | ├── src/ 11 | │ ├── index.ts # Main entry point 12 | │ ├── config.ts # Server configuration 13 | │ ├── tools/ # MCP tools implementation 14 | │ │ ├── index-tools.ts # Index management tools 15 | │ │ ├── document-tools.ts # Document management tools 16 | │ │ ├── search-tools.ts # Search tools 17 | │ │ ├── settings-tools.ts # Settings management tools 18 | │ │ ├── task-tools.ts # Task management tools 19 | │ │ ├── vector-tools.ts # Vector search tools 20 | │ │ └── system-tools.ts # System tools (health, stats, etc.) 21 | │ └── utils/ # Utility functions 22 | │ ├── api-client.ts # Meilisearch API client 23 | │ └── error-handler.ts # Error handling utilities 24 | ``` 25 | 26 | ## Implementation Phases 27 | 28 | ### Phase 1: Project Setup and Refactoring 29 | 1. Create the directory structure 30 | 2. Extract configuration into config.ts 31 | 3. Create utility modules for API client and error handling 32 | 4. Refactor the existing monolithic implementation into separate modules 33 | 34 | ### Phase 2: Core Functionality Implementation 35 | 1. **Index Management Tools** 36 | - List indexes 37 | - Get index information 38 | - Create index 39 | - Update index 40 | - Delete index 41 | - Get index stats 42 | 43 | 2. **Document Management Tools** 44 | - Add/update documents 45 | - Get documents 46 | - Delete documents 47 | - Batch document operations 48 | 49 | 3. **Search Tools** 50 | - Basic search 51 | - Search with filters 52 | - Search with sorting 53 | - Faceted search 54 | 55 | ### Phase 3: Advanced Functionality 56 | 1. **Settings Management Tools** 57 | - Get index settings 58 | - Update settings 59 | - Reset settings 60 | - Configure specific settings (synonyms, stop words, etc.) 61 | 62 | 2. **Task Management Tools** 63 | - Get tasks 64 | - Get task by ID 65 | - Cancel tasks 66 | 67 | 3. **System Tools** 68 | - Health check 69 | - Version information 70 | - Stats 71 | 72 | 4. **Vector Search Tools** 73 | - Configure embedders 74 | - Perform vector search 75 | - Hybrid search 76 | - Vector search with filters 77 | 78 | ## Testing and Documentation 79 | 1. Create unit tests for each module 80 | 2. Add integration tests for end-to-end functionality 81 | 3. Update README with detailed usage instructions 82 | 4. Create example scripts and configurations 83 | 84 | ## Timeline 85 | - Phase 1: 1 day 86 | - Phase 2: 2-3 days 87 | - Phase 3: 2-3 days 88 | - Testing and Documentation: 1-2 days 89 | 90 | Total estimated time: 6-9 days 91 | 92 | ## Dependencies 93 | - @modelcontextprotocol/sdk: For MCP server implementation 94 | - axios: For HTTP requests to Meilisearch API 95 | - zod: For parameter validation 96 | 97 | ## Notes 98 | - Vector search functionality requires Meilisearch to have vector search enabled as an experimental feature 99 | - The implementation will be backward compatible with existing clients 100 | - All tools will include detailed error handling and descriptive responses 101 | ``` -------------------------------------------------------------------------------- /scripts/claude-desktop-setup.js: -------------------------------------------------------------------------------- ```javascript 1 | import fs from 'fs'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | /** 7 | * Claude for Desktop Setup Helper 8 | * 9 | * This script helps users set up the Meilisearch MCP server for use with Claude for Desktop. 10 | * It generates the necessary configuration and provides instructions. 11 | */ 12 | 13 | // Get the directory name 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | const projectRoot = path.resolve(__dirname, '..'); 17 | 18 | // Get the absolute path to the built index.js file 19 | const indexPath = path.resolve(projectRoot, 'dist', 'index.js'); 20 | 21 | // Get user's home directory 22 | const homeDir = os.homedir(); 23 | 24 | // Default Claude for Desktop config path based on OS 25 | let claudeConfigPath; 26 | if (process.platform === 'darwin') { 27 | claudeConfigPath = path.join( 28 | homeDir, 29 | 'Library', 30 | 'Application Support', 31 | 'Claude', 32 | 'claude_desktop_config.json' 33 | ); 34 | } else if (process.platform === 'win32') { 35 | claudeConfigPath = path.join( 36 | homeDir, 37 | 'AppData', 38 | 'Roaming', 39 | 'Claude', 40 | 'claude_desktop_config.json' 41 | ); 42 | } else if (process.platform === 'linux') { 43 | claudeConfigPath = path.join( 44 | homeDir, 45 | '.config', 46 | 'Claude', 47 | 'claude_desktop_config.json' 48 | ); 49 | } else { 50 | console.error( 51 | 'Unsupported platform. Please manually configure Claude for Desktop.' 52 | ); 53 | process.exit(1); 54 | } 55 | 56 | // Read environment variables from .env file 57 | let meilisearchHost = process.env.MEILISEARCH_HOST; 58 | let meilisearchApiKey = process.env.MEILISEARCH_API_KEY; 59 | 60 | try { 61 | const envPath = path.resolve(projectRoot, '.env'); 62 | if (fs.existsSync(envPath)) { 63 | const envContent = fs.readFileSync(envPath, 'utf8'); 64 | const envLines = envContent.split('\n'); 65 | 66 | for (const line of envLines) { 67 | if (line.trim() && !line.startsWith('#')) { 68 | const [key, value] = line.split('='); 69 | if (key === 'MEILISEARCH_HOST' && value) { 70 | meilisearchHost = value.trim(); 71 | } else if (key === 'MEILISEARCH_API_KEY' && value) { 72 | meilisearchApiKey = value.trim(); 73 | } 74 | } 75 | } 76 | } 77 | } catch (error) { 78 | console.warn('Could not read .env file:', error.message); 79 | } 80 | 81 | // Generate Claude for Desktop configuration 82 | const claudeConfig = { 83 | mcpServers: { 84 | meilisearch: { 85 | command: 'node', 86 | args: [indexPath], 87 | env: { 88 | MEILISEARCH_HOST: meilisearchHost, 89 | MEILISEARCH_API_KEY: meilisearchApiKey, 90 | }, 91 | }, 92 | }, 93 | }; 94 | 95 | // Check if Claude config file exists 96 | let existingConfig = {}; 97 | try { 98 | if (fs.existsSync(claudeConfigPath)) { 99 | const configContent = fs.readFileSync(claudeConfigPath, 'utf8'); 100 | existingConfig = JSON.parse(configContent); 101 | console.log('Found existing Claude for Desktop configuration.'); 102 | } 103 | } catch (error) { 104 | console.warn( 105 | 'Could not read existing Claude for Desktop configuration:', 106 | error.message 107 | ); 108 | } 109 | 110 | // Merge configurations 111 | const mergedConfig = { 112 | ...existingConfig, 113 | mcpServers: { 114 | ...(existingConfig.mcpServers || {}), 115 | ...claudeConfig.mcpServers, 116 | }, 117 | }; 118 | 119 | // Output the configuration 120 | console.log('\n=== Claude for Desktop Configuration ===\n'); 121 | console.log(JSON.stringify(mergedConfig, null, 2)); 122 | console.log('\n'); 123 | 124 | // Ask if user wants to update the configuration 125 | console.log( 126 | 'To use this configuration with Claude for Desktop, you can either:' 127 | ); 128 | console.log( 129 | `1. Manually update your configuration file at: ${claudeConfigPath}` 130 | ); 131 | console.log('2. Run the following command to automatically update it:'); 132 | console.log( 133 | `\n node -e "require('fs').writeFileSync('${claudeConfigPath.replace( 134 | /\\/g, 135 | '\\\\' 136 | )}', JSON.stringify(${JSON.stringify(mergedConfig)}, null, 2))"\n` 137 | ); 138 | console.log( 139 | 'After updating the configuration, restart Claude for Desktop to apply the changes.' 140 | ); 141 | console.log( 142 | '\nYou can then use the Meilisearch MCP server with Claude by typing: "I want to use the Meilisearch MCP server."' 143 | ); 144 | ``` -------------------------------------------------------------------------------- /vector-search-guide.md: -------------------------------------------------------------------------------- ```markdown 1 | # Vector Search with Meilisearch MCP 2 | 3 | This guide explains how to use the vector search capabilities in the Meilisearch MCP server. Vector search allows for semantic similarity matching, enabling more sophisticated search experiences. 4 | 5 | ## Overview 6 | 7 | Vector search in Meilisearch enables: 8 | - Semantic search based on the meaning of content 9 | - Similar document recommendations 10 | - Hybrid search combining keyword and semantic results 11 | - Multi-modal search experiences 12 | 13 | ## Enabling Vector Search 14 | 15 | Vector search is an experimental feature in Meilisearch. Before using it, you must enable it: 16 | 17 | ``` 18 | # Enable vector search experimental feature 19 | enable-vector-search 20 | ``` 21 | 22 | ## Setting Up Vector Search 23 | 24 | ### 1. Configure Embedders 25 | 26 | First, configure an embedder for your index: 27 | 28 | ``` 29 | # Example: Configure OpenAI embedder 30 | update-embedders { 31 | "indexUid": "my-index", 32 | "embedders": { 33 | "openai-embedder": { 34 | "source": "openAi", 35 | "model": "text-embedding-3-small", 36 | "dimensions": 1536 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | Common embedder sources include: 43 | - `openAi` - OpenAI embeddings 44 | - `huggingFace` - HuggingFace models 45 | - `ollama` - Ollama local models 46 | - `rest` - Custom REST API endpoint 47 | - `userProvided` - Pre-computed embeddings 48 | 49 | ### 2. Add Documents with Vectors 50 | 51 | You can add documents with pre-computed vectors: 52 | 53 | ``` 54 | # Add documents with vector embeddings 55 | add-documents { 56 | "indexUid": "my-index", 57 | "documents": [ 58 | { 59 | "id": "1", 60 | "title": "Vector search guide", 61 | "content": "This is about vector search...", 62 | "_vectors": { 63 | "openai-embedder": [0.123, 0.456, ...] 64 | } 65 | } 66 | ] 67 | } 68 | ``` 69 | 70 | Alternatively, if you've configured an embedder, Meilisearch can generate the embeddings automatically from your text fields. 71 | 72 | ## Performing Vector Searches 73 | 74 | ### Basic Vector Search 75 | 76 | If you have a vector representation of your query: 77 | 78 | ``` 79 | # Vector search 80 | search { 81 | "indexUid": "my-index", 82 | "vector": [0.123, 0.456, ...], 83 | "limit": 10 84 | } 85 | ``` 86 | 87 | ### Hybrid Search 88 | 89 | Combine traditional keyword search with vector search: 90 | 91 | ``` 92 | # Hybrid search 93 | search { 94 | "indexUid": "my-index", 95 | "q": "machine learning techniques", 96 | "vector": [0.123, 0.456, ...], 97 | "hybridEmbedder": "openai-embedder", 98 | "hybridSemanticRatio": 0.7 99 | } 100 | ``` 101 | 102 | The `hybridSemanticRatio` controls the balance between semantic (vector) and lexical (keyword) search: 103 | - 0.0: Only keyword search 104 | - 1.0: Only vector search 105 | - 0.5: Equal weight to both 106 | 107 | ### Finding Similar Documents 108 | 109 | Find documents similar to an existing document: 110 | 111 | ``` 112 | # Similar documents search 113 | similar-documents { 114 | "indexUid": "my-index", 115 | "id": "doc123", 116 | "embedder": "openai-embedder", 117 | "limit": 5 118 | } 119 | ``` 120 | 121 | ## Multi-Index Vector Search 122 | 123 | Perform vector searches across multiple indexes: 124 | 125 | ``` 126 | # Multi-index vector search 127 | multi-search { 128 | "queries": [ 129 | { 130 | "indexUid": "products", 131 | "vector": [0.1, 0.2, ...], 132 | "hybridEmbedder": "openai-embedder", 133 | "limit": 5 134 | }, 135 | { 136 | "indexUid": "articles", 137 | "vector": [0.1, 0.2, ...], 138 | "hybridEmbedder": "openai-embedder", 139 | "limit": 5 140 | } 141 | ], 142 | "federation": { 143 | "limit": 10 144 | } 145 | } 146 | ``` 147 | 148 | ## Best Practices 149 | 150 | 1. **Choose the right embedder**: Different models have different strengths and capabilities. 151 | 152 | 2. **Experiment with hybrid ratios**: The ideal balance between vector and keyword search depends on your content and use case. 153 | 154 | 3. **Pre-compute embeddings** when possible to improve indexing performance. 155 | 156 | 4. **Use filters** with vector search to constrain results to relevant subsets. 157 | 158 | 5. **Consider reranking** for critical applications to improve result quality. 159 | 160 | ## Potential Use Cases 161 | 162 | - **Semantic code search**: Find code examples by describing functionality 163 | - **Similar product recommendations**: "Show me products like this one" 164 | - **Research document similarity**: Find related academic papers or reports 165 | - **Natural language queries**: Search for concepts rather than exact keywords 166 | - **Content discovery**: Find content with similar themes or topics 167 | 168 | ## Limitations 169 | 170 | - Vector search is an experimental feature and may change in future Meilisearch releases 171 | - Vector search performs best with larger datasets where semantic similarity matters 172 | - Compute requirements increase with vector dimensions and dataset size 173 | ``` -------------------------------------------------------------------------------- /examples/movies-demo.js: -------------------------------------------------------------------------------- ```javascript 1 | import axios from 'axios'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | /** 6 | * Meilisearch MCP Server - Movies Demo 7 | * 8 | * This script demonstrates how to use the Meilisearch MCP server with a sample movie dataset. 9 | * It creates an index, adds documents, configures settings, and performs searches. 10 | */ 11 | 12 | // Get the directory name 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | // Configuration 17 | const MEILISEARCH_HOST = 18 | process.env.MEILISEARCH_HOST || 'http://localhost:7700'; 19 | const MEILISEARCH_API_KEY = process.env.MEILISEARCH_API_KEY || ''; 20 | 21 | // Create an axios instance for Meilisearch 22 | const meilisearch = axios.create({ 23 | baseURL: MEILISEARCH_HOST, 24 | headers: MEILISEARCH_API_KEY 25 | ? { Authorization: `Bearer ${MEILISEARCH_API_KEY}` } 26 | : {}, 27 | timeout: 5000, 28 | }); 29 | 30 | // Sample movie data 31 | const movies = [ 32 | { 33 | id: 1, 34 | title: 'The Shawshank Redemption', 35 | director: 'Frank Darabont', 36 | genres: ['Drama'], 37 | year: 1994, 38 | rating: 9.3, 39 | }, 40 | { 41 | id: 2, 42 | title: 'The Godfather', 43 | director: 'Francis Ford Coppola', 44 | genres: ['Crime', 'Drama'], 45 | year: 1972, 46 | rating: 9.2, 47 | }, 48 | { 49 | id: 3, 50 | title: 'The Dark Knight', 51 | director: 'Christopher Nolan', 52 | genres: ['Action', 'Crime', 'Drama'], 53 | year: 2008, 54 | rating: 9.0, 55 | }, 56 | { 57 | id: 4, 58 | title: 'Pulp Fiction', 59 | director: 'Quentin Tarantino', 60 | genres: ['Crime', 'Drama'], 61 | year: 1994, 62 | rating: 8.9, 63 | }, 64 | { 65 | id: 5, 66 | title: 'The Lord of the Rings: The Return of the King', 67 | director: 'Peter Jackson', 68 | genres: ['Action', 'Adventure', 'Drama'], 69 | year: 2003, 70 | rating: 8.9, 71 | }, 72 | ]; 73 | 74 | /** 75 | * Create a movies index 76 | */ 77 | async function createMoviesIndex() { 78 | try { 79 | const response = await meilisearch.post('/indexes', { 80 | uid: 'movies', 81 | primaryKey: 'id', 82 | }); 83 | console.log('Index created:', response.data); 84 | return response.data; 85 | } catch (error) { 86 | console.error( 87 | 'Error creating index:', 88 | error.response?.data || error.message 89 | ); 90 | throw error; 91 | } 92 | } 93 | 94 | /** 95 | * Add movies to the index 96 | */ 97 | async function addMovies() { 98 | try { 99 | const response = await meilisearch.post( 100 | '/indexes/movies/documents', 101 | movies 102 | ); 103 | console.log('Movies added:', response.data); 104 | return response.data; 105 | } catch (error) { 106 | console.error( 107 | 'Error adding movies:', 108 | error.response?.data || error.message 109 | ); 110 | throw error; 111 | } 112 | } 113 | 114 | /** 115 | * Update index settings 116 | */ 117 | async function updateSettings() { 118 | try { 119 | const settings = { 120 | searchableAttributes: ['title', 'director', 'genres'], 121 | filterableAttributes: ['genres', 'year', 'rating'], 122 | sortableAttributes: ['year', 'rating'], 123 | }; 124 | 125 | const response = await meilisearch.patch( 126 | '/indexes/movies/settings', 127 | settings 128 | ); 129 | console.log('Settings updated:', response.data); 130 | return response.data; 131 | } catch (error) { 132 | console.error( 133 | 'Error updating settings:', 134 | error.response?.data || error.message 135 | ); 136 | throw error; 137 | } 138 | } 139 | 140 | /** 141 | * Search for movies 142 | */ 143 | async function searchMovies(query, filters = null) { 144 | try { 145 | const params = { q: query }; 146 | if (filters) { 147 | params.filter = filters; 148 | } 149 | 150 | const response = await meilisearch.post('/indexes/movies/search', params); 151 | console.log(`Search results for "${query}":`, response.data.hits); 152 | return response.data; 153 | } catch (error) { 154 | console.error( 155 | 'Error searching movies:', 156 | error.response?.data || error.message 157 | ); 158 | throw error; 159 | } 160 | } 161 | 162 | /** 163 | * Wait for a task to complete 164 | */ 165 | async function waitForTask(taskId) { 166 | try { 167 | let task; 168 | do { 169 | const response = await meilisearch.get(`/tasks/${taskId}`); 170 | task = response.data; 171 | 172 | if (['succeeded', 'failed', 'canceled'].includes(task.status)) { 173 | break; 174 | } 175 | 176 | console.log(`Task ${taskId} is ${task.status}. Waiting...`); 177 | await new Promise((resolve) => setTimeout(resolve, 500)); 178 | } while (true); 179 | 180 | console.log(`Task ${taskId} ${task.status}`); 181 | return task; 182 | } catch (error) { 183 | console.error( 184 | 'Error waiting for task:', 185 | error.response?.data || error.message 186 | ); 187 | throw error; 188 | } 189 | } 190 | 191 | /** 192 | * Run the demo 193 | */ 194 | async function runDemo() { 195 | try { 196 | console.log('Starting Meilisearch Movies Demo...'); 197 | 198 | // Create index 199 | const createIndexTask = await createMoviesIndex(); 200 | await waitForTask(createIndexTask.taskUid); 201 | 202 | // Add movies 203 | const addMoviesTask = await addMovies(); 204 | await waitForTask(addMoviesTask.taskUid); 205 | 206 | // Update settings 207 | const updateSettingsTask = await updateSettings(); 208 | await waitForTask(updateSettingsTask.taskUid); 209 | 210 | // Perform searches 211 | await searchMovies('dark'); 212 | await searchMovies('', 'genres = Drama AND year > 2000'); 213 | await searchMovies('', 'rating > 9'); 214 | 215 | console.log('Demo completed successfully!'); 216 | } catch (error) { 217 | console.error('Demo failed:', error); 218 | } 219 | } 220 | 221 | // Run the demo 222 | runDemo(); 223 | ``` -------------------------------------------------------------------------------- /src/tools/index-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch Index Management Tools 9 | * 10 | * This module implements MCP tools for managing Meilisearch indexes. 11 | */ 12 | 13 | // Define types for index parameters 14 | interface ListIndexesParams { 15 | limit?: number; 16 | offset?: number; 17 | } 18 | 19 | interface GetIndexParams { 20 | indexUid: string; 21 | } 22 | 23 | interface CreateIndexParams { 24 | indexUid: string; 25 | primaryKey?: string; 26 | } 27 | 28 | interface UpdateIndexParams { 29 | indexUid: string; 30 | primaryKey: string; 31 | } 32 | 33 | interface DeleteIndexParams { 34 | indexUid: string; 35 | } 36 | 37 | interface SwapIndexesParams { 38 | indexes: string; 39 | } 40 | 41 | /** 42 | * Register index management tools with the MCP server 43 | * 44 | * @param server - The MCP server instance 45 | */ 46 | export const registerIndexTools = (server: McpServer) => { 47 | // List all indexes 48 | server.tool( 49 | 'list-indexes', 50 | 'List all indexes in the Meilisearch instance', 51 | { 52 | limit: z.number().min(1).max(100).optional().describe('Maximum number of indexes to return'), 53 | offset: z.number().min(0).optional().describe('Number of indexes to skip'), 54 | }, 55 | async ({ limit, offset }: ListIndexesParams) => { 56 | try { 57 | const response = await apiClient.get('/indexes', { 58 | params: { 59 | limit, 60 | offset, 61 | }, 62 | }); 63 | return { 64 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 65 | }; 66 | } catch (error) { 67 | return createErrorResponse(error); 68 | } 69 | } 70 | ); 71 | 72 | // Get index information 73 | server.tool( 74 | 'get-index', 75 | 'Get information about a specific Meilisearch index', 76 | { 77 | indexUid: z.string().describe('Unique identifier of the index'), 78 | }, 79 | async ({ indexUid }: GetIndexParams) => { 80 | try { 81 | const response = await apiClient.get(`/indexes/${indexUid}`); 82 | return { 83 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 84 | }; 85 | } catch (error) { 86 | return createErrorResponse(error); 87 | } 88 | } 89 | ); 90 | 91 | // Create a new index 92 | server.tool( 93 | 'create-index', 94 | 'Create a new Meilisearch index', 95 | { 96 | indexUid: z.string().describe('Unique identifier for the new index'), 97 | primaryKey: z.string().optional().describe('Primary key for the index'), 98 | }, 99 | async ({ indexUid, primaryKey }: CreateIndexParams) => { 100 | try { 101 | const response = await apiClient.post('/indexes', { 102 | uid: indexUid, 103 | primaryKey, 104 | }); 105 | return { 106 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 107 | }; 108 | } catch (error) { 109 | return createErrorResponse(error); 110 | } 111 | } 112 | ); 113 | 114 | // Update an index 115 | server.tool( 116 | 'update-index', 117 | 'Update a Meilisearch index (currently only supports updating the primary key)', 118 | { 119 | indexUid: z.string().describe('Unique identifier of the index'), 120 | primaryKey: z.string().describe('New primary key for the index'), 121 | }, 122 | async ({ indexUid, primaryKey }: UpdateIndexParams) => { 123 | try { 124 | const response = await apiClient.patch(`/indexes/${indexUid}`, { 125 | primaryKey, 126 | }); 127 | return { 128 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 129 | }; 130 | } catch (error) { 131 | return createErrorResponse(error); 132 | } 133 | } 134 | ); 135 | 136 | // Delete an index 137 | server.tool( 138 | 'delete-index', 139 | 'Delete a Meilisearch index', 140 | { 141 | indexUid: z.string().describe('Unique identifier of the index to delete'), 142 | }, 143 | async ({ indexUid }: DeleteIndexParams) => { 144 | try { 145 | const response = await apiClient.delete(`/indexes/${indexUid}`); 146 | return { 147 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 148 | }; 149 | } catch (error) { 150 | return createErrorResponse(error); 151 | } 152 | } 153 | ); 154 | 155 | // Swap indexes 156 | server.tool( 157 | 'swap-indexes', 158 | 'Swap two or more indexes in Meilisearch', 159 | { 160 | indexes: z.string().describe('JSON array of index pairs to swap, e.g. [["movies", "movies_new"]]'), 161 | }, 162 | async ({ indexes }: SwapIndexesParams) => { 163 | try { 164 | // Parse the indexes string to ensure it's valid JSON 165 | const parsedIndexes = JSON.parse(indexes); 166 | 167 | // Ensure indexes is an array of arrays 168 | if (!Array.isArray(parsedIndexes) || !parsedIndexes.every(pair => Array.isArray(pair) && pair.length === 2)) { 169 | return { 170 | isError: true, 171 | content: [{ type: 'text', text: 'Indexes must be a JSON array of pairs, e.g. [["movies", "movies_new"]]' }], 172 | }; 173 | } 174 | 175 | const response = await apiClient.post('/swap-indexes', parsedIndexes); 176 | return { 177 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 178 | }; 179 | } catch (error) { 180 | return createErrorResponse(error); 181 | } 182 | } 183 | ); 184 | }; 185 | 186 | export default registerIndexTools; 187 | ``` -------------------------------------------------------------------------------- /src/tools/search-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch Search Tools 9 | * 10 | * This module implements MCP tools for searching in Meilisearch indexes. 11 | */ 12 | 13 | // Define types for search parameters 14 | interface SearchParams { 15 | indexUid: string; 16 | q: string; 17 | limit?: number; 18 | offset?: number; 19 | filter?: string; 20 | sort?: string[]; 21 | facets?: string[]; 22 | attributesToRetrieve?: string[]; 23 | attributesToCrop?: string[]; 24 | cropLength?: number; 25 | attributesToHighlight?: string[]; 26 | highlightPreTag?: string; 27 | highlightPostTag?: string; 28 | showMatchesPosition?: boolean; 29 | matchingStrategy?: string; 30 | } 31 | 32 | interface MultiSearchParams { 33 | searches: string; 34 | } 35 | 36 | /** 37 | * Register search tools with the MCP server 38 | * 39 | * @param server - The MCP server instance 40 | */ 41 | export const registerSearchTools = (server: McpServer) => { 42 | // Search in an index 43 | server.tool( 44 | 'search', 45 | 'Search for documents in a Meilisearch index', 46 | { 47 | indexUid: z.string().describe('Unique identifier of the index'), 48 | q: z.string().describe('Search query'), 49 | limit: z.number().min(1).max(1000).optional().describe('Maximum number of results to return (default: 20)'), 50 | offset: z.number().min(0).optional().describe('Number of results to skip (default: 0)'), 51 | filter: z.string().optional().describe('Filter query to apply'), 52 | sort: z.array(z.string()).optional().describe('Attributes to sort by, e.g. ["price:asc"]'), 53 | facets: z.array(z.string()).optional().describe('Facets to return'), 54 | attributesToRetrieve: z.array(z.string()).optional().describe('Attributes to include in results'), 55 | attributesToCrop: z.array(z.string()).optional().describe('Attributes to crop'), 56 | cropLength: z.number().optional().describe('Length at which to crop cropped attributes'), 57 | attributesToHighlight: z.array(z.string()).optional().describe('Attributes to highlight'), 58 | highlightPreTag: z.string().optional().describe('Tag to insert before highlighted text'), 59 | highlightPostTag: z.string().optional().describe('Tag to insert after highlighted text'), 60 | showMatchesPosition: z.boolean().optional().describe('Whether to include match positions in results'), 61 | matchingStrategy: z.string().optional().describe("Matching strategy: 'all' or 'last'"), 62 | }, 63 | async ({ 64 | indexUid, 65 | q, 66 | limit, 67 | offset, 68 | filter, 69 | sort, 70 | facets, 71 | attributesToRetrieve, 72 | attributesToCrop, 73 | cropLength, 74 | attributesToHighlight, 75 | highlightPreTag, 76 | highlightPostTag, 77 | showMatchesPosition, 78 | matchingStrategy 79 | }: SearchParams) => { 80 | try { 81 | const response = await apiClient.post(`/indexes/${indexUid}/search`, { 82 | q, 83 | limit, 84 | offset, 85 | filter, 86 | sort, 87 | facets, 88 | attributesToRetrieve, 89 | attributesToCrop, 90 | cropLength, 91 | attributesToHighlight, 92 | highlightPreTag, 93 | highlightPostTag, 94 | showMatchesPosition, 95 | matchingStrategy, 96 | }); 97 | return { 98 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 99 | }; 100 | } catch (error) { 101 | return createErrorResponse(error); 102 | } 103 | } 104 | ); 105 | 106 | // Multi-search across multiple indexes 107 | server.tool( 108 | 'multi-search', 109 | 'Perform multiple searches in one request', 110 | { 111 | searches: z.string().describe('JSON array of search queries, each with indexUid and q fields'), 112 | }, 113 | async ({ searches }: MultiSearchParams) => { 114 | try { 115 | // Parse the searches string to ensure it's valid JSON 116 | const parsedSearches = JSON.parse(searches); 117 | 118 | // Ensure searches is an array 119 | if (!Array.isArray(parsedSearches)) { 120 | return { 121 | isError: true, 122 | content: [{ type: 'text', text: 'Searches must be a JSON array' }], 123 | }; 124 | } 125 | 126 | // Ensure each search has at least indexUid 127 | for (const search of parsedSearches) { 128 | if (!search.indexUid) { 129 | return { 130 | isError: true, 131 | content: [{ type: 'text', text: 'Each search must have an indexUid field' }], 132 | }; 133 | } 134 | } 135 | 136 | const response = await apiClient.post('/multi-search', { 137 | queries: parsedSearches, 138 | }); 139 | return { 140 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 141 | }; 142 | } catch (error) { 143 | return createErrorResponse(error); 144 | } 145 | } 146 | ); 147 | 148 | // Facet search 149 | server.tool( 150 | 'facet-search', 151 | 'Search for facet values matching specific criteria', 152 | { 153 | indexUid: z.string().describe('Unique identifier of the index'), 154 | facetName: z.string().describe('Name of the facet to search'), 155 | facetQuery: z.string().optional().describe('Query to match against facet values'), 156 | filter: z.string().optional().describe('Filter to apply to the base search'), 157 | }, 158 | async ({ indexUid, facetName, facetQuery, filter }) => { 159 | try { 160 | const params: Record<string, any> = { 161 | facetName, 162 | }; 163 | 164 | if (facetQuery !== undefined) params.facetQuery = facetQuery; 165 | if (filter) params.filter = filter; 166 | 167 | const response = await apiClient.post(`/indexes/${indexUid}/facet-search`, params); 168 | return { 169 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 170 | }; 171 | } catch (error) { 172 | return createErrorResponse(error); 173 | } 174 | } 175 | ); 176 | }; 177 | 178 | export default registerSearchTools; 179 | ``` -------------------------------------------------------------------------------- /src/tools/system-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch System Tools 9 | * 10 | * This module implements MCP tools for system operations in Meilisearch. 11 | */ 12 | 13 | /** 14 | * Register system tools with the MCP server 15 | * 16 | * @param server - The MCP server instance 17 | */ 18 | export const registerSystemTools = (server: McpServer) => { 19 | // Get health status 20 | server.tool( 21 | 'health', 22 | 'Check if the Meilisearch server is healthy', 23 | {}, 24 | async () => { 25 | try { 26 | const response = await apiClient.get('/health'); 27 | return { 28 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 29 | }; 30 | } catch (error) { 31 | return createErrorResponse(error); 32 | } 33 | } 34 | ); 35 | 36 | // Get version information 37 | server.tool( 38 | 'version', 39 | 'Get the version information of the Meilisearch server', 40 | {}, 41 | async () => { 42 | try { 43 | const response = await apiClient.get('/version'); 44 | return { 45 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 46 | }; 47 | } catch (error) { 48 | return createErrorResponse(error); 49 | } 50 | } 51 | ); 52 | 53 | // Get system information 54 | server.tool( 55 | 'info', 56 | 'Get the system information of the Meilisearch server', 57 | {}, 58 | async () => { 59 | try { 60 | const response = await apiClient.get('/'); 61 | return { 62 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 63 | }; 64 | } catch (error) { 65 | return createErrorResponse(error); 66 | } 67 | } 68 | ); 69 | 70 | // Get statistics 71 | server.tool( 72 | 'stats', 73 | 'Get statistics about all indexes or a specific index', 74 | { 75 | indexUid: z.string().optional().describe('Unique identifier of the index (optional, if not provided stats for all indexes will be returned)'), 76 | }, 77 | async ({ indexUid }) => { 78 | try { 79 | const endpoint = indexUid ? `/indexes/${indexUid}/stats` : '/stats'; 80 | const response = await apiClient.get(endpoint); 81 | return { 82 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 83 | }; 84 | } catch (error) { 85 | return createErrorResponse(error); 86 | } 87 | } 88 | ); 89 | 90 | // Get all tasks (with optional filtering) 91 | server.tool( 92 | 'get-tasks', 93 | 'Get information about tasks with optional filtering', 94 | { 95 | limit: z.number().min(0).optional().describe('Maximum number of tasks to return'), 96 | from: z.number().min(0).optional().describe('Task uid from which to start fetching'), 97 | status: z.enum(['enqueued', 'processing', 'succeeded', 'failed', 'canceled']).optional().describe('Status of tasks to return'), 98 | type: z.enum(['indexCreation', 'indexUpdate', 'indexDeletion', 'documentAddition', 'documentUpdate', 'documentDeletion', 'settingsUpdate', 'dumpCreation', 'taskCancelation']).optional().describe('Type of tasks to return'), 99 | indexUids: z.array(z.string()).optional().describe('UIDs of the indexes on which tasks were performed'), 100 | }, 101 | async ({ limit, from, status, type, indexUids }) => { 102 | try { 103 | const params: Record<string, any> = {}; 104 | if (limit !== undefined) params.limit = limit; 105 | if (from !== undefined) params.from = from; 106 | if (status) params.status = status; 107 | if (type) params.type = type; 108 | if (indexUids && indexUids.length > 0) params.indexUids = indexUids.join(','); 109 | 110 | const response = await apiClient.get('/tasks', { params }); 111 | return { 112 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 113 | }; 114 | } catch (error) { 115 | return createErrorResponse(error); 116 | } 117 | } 118 | ); 119 | 120 | // Delete tasks 121 | server.tool( 122 | 'delete-tasks', 123 | 'Delete tasks based on provided filters', 124 | { 125 | statuses: z.array(z.enum(['succeeded', 'failed', 'canceled'])).optional().describe('Statuses of tasks to delete'), 126 | types: z.array(z.enum(['indexCreation', 'indexUpdate', 'indexDeletion', 'documentAddition', 'documentUpdate', 'documentDeletion', 'settingsUpdate', 'dumpCreation', 'taskCancelation'])).optional().describe('Types of tasks to delete'), 127 | indexUids: z.array(z.string()).optional().describe('UIDs of the indexes on which tasks to delete were performed'), 128 | uids: z.array(z.number()).optional().describe('UIDs of the tasks to delete'), 129 | canceledBy: z.array(z.number()).optional().describe('UIDs of the tasks that canceled tasks to delete'), 130 | beforeUid: z.number().optional().describe('Delete tasks whose uid is before this value'), 131 | beforeStartedAt: z.string().optional().describe('Delete tasks that started processing before this date (ISO 8601 format)'), 132 | beforeFinishedAt: z.string().optional().describe('Delete tasks that finished processing before this date (ISO 8601 format)'), 133 | }, 134 | async ({ statuses, types, indexUids, uids, canceledBy, beforeUid, beforeStartedAt, beforeFinishedAt }) => { 135 | try { 136 | const body: Record<string, any> = {}; 137 | if (statuses && statuses.length > 0) body.statuses = statuses; 138 | if (types && types.length > 0) body.types = types; 139 | if (indexUids && indexUids.length > 0) body.indexUids = indexUids; 140 | if (uids && uids.length > 0) body.uids = uids; 141 | if (canceledBy && canceledBy.length > 0) body.canceledBy = canceledBy; 142 | if (beforeUid !== undefined) body.beforeUid = beforeUid; 143 | if (beforeStartedAt) body.beforeStartedAt = beforeStartedAt; 144 | if (beforeFinishedAt) body.beforeFinishedAt = beforeFinishedAt; 145 | 146 | const response = await apiClient.post('/tasks/delete', body); 147 | return { 148 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 149 | }; 150 | } catch (error) { 151 | return createErrorResponse(error); 152 | } 153 | } 154 | ); 155 | }; 156 | 157 | export default registerSystemTools; 158 | ``` -------------------------------------------------------------------------------- /src/tools/task-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch Task Management Tools 9 | * 10 | * This module implements MCP tools for managing tasks in Meilisearch. 11 | */ 12 | 13 | // Define types for task parameters 14 | interface ListTasksParams { 15 | limit?: number; 16 | from?: number; 17 | statuses?: string[]; 18 | types?: string[]; 19 | indexUids?: string[]; 20 | uids?: number[]; 21 | } 22 | 23 | interface GetTaskParams { 24 | taskUid: number; 25 | } 26 | 27 | interface CancelTasksParams { 28 | statuses?: string[]; 29 | types?: string[]; 30 | indexUids?: string[]; 31 | uids?: number[]; 32 | } 33 | 34 | interface WaitForTaskParams { 35 | taskUid: number; 36 | timeoutMs?: number; 37 | intervalMs?: number; 38 | } 39 | 40 | /** 41 | * Register task management tools with the MCP server 42 | * 43 | * @param server - The MCP server instance 44 | */ 45 | export const registerTaskTools = (server: McpServer) => { 46 | // Get all tasks 47 | server.tool( 48 | "list-tasks", 49 | "List tasks with optional filtering", 50 | { 51 | limit: z.number().min(0).optional().describe("Maximum number of tasks to return"), 52 | from: z.number().min(0).optional().describe("Task uid from which to start fetching"), 53 | statuses: z.array(z.enum(["enqueued", "processing", "succeeded", "failed", "canceled"])).optional().describe("Statuses of tasks to return"), 54 | types: z.array(z.enum(["indexCreation", "indexUpdate", "indexDeletion", "documentAddition", "documentUpdate", "documentDeletion", "settingsUpdate", "dumpCreation", "taskCancelation"])).optional().describe("Types of tasks to return"), 55 | indexUids: z.array(z.string()).optional().describe("UIDs of the indexes on which tasks were performed"), 56 | uids: z.array(z.number()).optional().describe("UIDs of specific tasks to return"), 57 | }, 58 | async ({ limit, from, statuses, types, indexUids, uids }: ListTasksParams) => { 59 | try { 60 | const params: Record<string, any> = {}; 61 | if (limit !== undefined) params.limit = limit; 62 | if (from !== undefined) params.from = from; 63 | if (statuses && statuses.length > 0) params.statuses = statuses.join(','); 64 | if (types && types.length > 0) params.types = types.join(','); 65 | if (indexUids && indexUids.length > 0) params.indexUids = indexUids.join(','); 66 | if (uids && uids.length > 0) params.uids = uids.join(','); 67 | 68 | const response = await apiClient.get('/tasks', { params }); 69 | return { 70 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 71 | }; 72 | } catch (error) { 73 | return createErrorResponse(error); 74 | } 75 | } 76 | ); 77 | 78 | // Get a specific task 79 | server.tool( 80 | "get-task", 81 | "Get information about a specific task", 82 | { 83 | taskUid: z.number().describe("Unique identifier of the task"), 84 | }, 85 | async ({ taskUid }: GetTaskParams) => { 86 | try { 87 | const response = await apiClient.get(`/tasks/${taskUid}`); 88 | return { 89 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 90 | }; 91 | } catch (error) { 92 | return createErrorResponse(error); 93 | } 94 | } 95 | ); 96 | 97 | // Cancel tasks 98 | server.tool( 99 | "cancel-tasks", 100 | "Cancel tasks based on provided filters", 101 | { 102 | statuses: z.array(z.enum(["enqueued", "processing"])).optional().describe("Statuses of tasks to cancel"), 103 | types: z.array(z.enum(["indexCreation", "indexUpdate", "indexDeletion", "documentAddition", "documentUpdate", "documentDeletion", "settingsUpdate", "dumpCreation", "taskCancelation"])).optional().describe("Types of tasks to cancel"), 104 | indexUids: z.array(z.string()).optional().describe("UIDs of the indexes on which tasks to cancel were performed"), 105 | uids: z.array(z.number()).optional().describe("UIDs of the tasks to cancel"), 106 | }, 107 | async ({ statuses, types, indexUids, uids }: CancelTasksParams) => { 108 | try { 109 | const body: Record<string, any> = {}; 110 | if (statuses && statuses.length > 0) body.statuses = statuses; 111 | if (types && types.length > 0) body.types = types; 112 | if (indexUids && indexUids.length > 0) body.indexUids = indexUids; 113 | if (uids && uids.length > 0) body.uids = uids; 114 | 115 | const response = await apiClient.post('/tasks/cancel', body); 116 | return { 117 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 118 | }; 119 | } catch (error) { 120 | return createErrorResponse(error); 121 | } 122 | } 123 | ); 124 | 125 | // Wait for a task to complete 126 | server.tool( 127 | "wait-for-task", 128 | "Wait for a specific task to complete", 129 | { 130 | taskUid: z.number().describe("Unique identifier of the task to wait for"), 131 | timeoutMs: z.number().min(0).optional().describe("Maximum time to wait in milliseconds (default: 5000)"), 132 | intervalMs: z.number().min(100).optional().describe("Polling interval in milliseconds (default: 500)"), 133 | }, 134 | async ({ taskUid, timeoutMs = 5000, intervalMs = 500 }: WaitForTaskParams) => { 135 | try { 136 | const startTime = Date.now(); 137 | let taskCompleted = false; 138 | let taskData = null; 139 | 140 | while (!taskCompleted && (Date.now() - startTime < timeoutMs)) { 141 | // Fetch the current task status 142 | const response = await apiClient.get(`/tasks/${taskUid}`); 143 | taskData = response.data; 144 | 145 | // Check if the task has completed 146 | if (["succeeded", "failed", "canceled"].includes(taskData.status)) { 147 | taskCompleted = true; 148 | } else { 149 | // Wait for the polling interval 150 | await new Promise(resolve => setTimeout(resolve, intervalMs)); 151 | } 152 | } 153 | 154 | if (!taskCompleted) { 155 | return { 156 | content: [{ type: "text", text: `Task ${taskUid} did not complete within the timeout period of ${timeoutMs}ms` }], 157 | }; 158 | } 159 | 160 | return { 161 | content: [{ type: "text", text: JSON.stringify(taskData, null, 2) }], 162 | }; 163 | } catch (error) { 164 | return createErrorResponse(error); 165 | } 166 | } 167 | ); 168 | }; 169 | 170 | export default registerTaskTools; 171 | ``` -------------------------------------------------------------------------------- /src/tools/vector-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch Vector Search Tools 9 | * 10 | * This module implements MCP tools for vector search capabilities in Meilisearch. 11 | * Note: Vector search is an experimental feature in Meilisearch. 12 | */ 13 | 14 | /** 15 | * Register vector search tools with the MCP server 16 | * 17 | * @param server - The MCP server instance 18 | */ 19 | export const registerVectorTools = (server: McpServer) => { 20 | // Enable vector search experimental feature 21 | server.tool( 22 | "enable-vector-search", 23 | "Enable the vector search experimental feature in Meilisearch", 24 | {}, 25 | async () => { 26 | try { 27 | const response = await apiClient.post('/experimental-features', { 28 | vectorStore: true, 29 | }); 30 | return { 31 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 32 | }; 33 | } catch (error) { 34 | return createErrorResponse(error); 35 | } 36 | } 37 | ); 38 | 39 | // Get experimental features status 40 | server.tool( 41 | "get-experimental-features", 42 | "Get the status of experimental features in Meilisearch", 43 | {}, 44 | async () => { 45 | try { 46 | const response = await apiClient.get('/experimental-features'); 47 | return { 48 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 49 | }; 50 | } catch (error) { 51 | return createErrorResponse(error); 52 | } 53 | } 54 | ); 55 | 56 | // Update embedders configuration 57 | server.tool( 58 | "update-embedders", 59 | "Configure embedders for vector search", 60 | { 61 | indexUid: z.string().describe("Unique identifier of the index"), 62 | embedders: z.string().describe("JSON object containing embedder configurations"), 63 | }, 64 | async ({ indexUid, embedders }) => { 65 | try { 66 | // Parse the embedders string to ensure it's valid JSON 67 | const parsedEmbedders = JSON.parse(embedders); 68 | 69 | // Ensure embedders is an object 70 | if (typeof parsedEmbedders !== 'object' || parsedEmbedders === null || Array.isArray(parsedEmbedders)) { 71 | return { 72 | isError: true, 73 | content: [{ type: "text", text: "Embedders must be a JSON object" }], 74 | }; 75 | } 76 | 77 | const response = await apiClient.patch(`/indexes/${indexUid}/settings/embedders`, parsedEmbedders); 78 | return { 79 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 80 | }; 81 | } catch (error) { 82 | return createErrorResponse(error); 83 | } 84 | } 85 | ); 86 | 87 | // Get embedders configuration 88 | server.tool( 89 | "get-embedders", 90 | "Get the embedders configuration for an index", 91 | { 92 | indexUid: z.string().describe("Unique identifier of the index"), 93 | }, 94 | async ({ indexUid }) => { 95 | try { 96 | const response = await apiClient.get(`/indexes/${indexUid}/settings/embedders`); 97 | return { 98 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 99 | }; 100 | } catch (error) { 101 | return createErrorResponse(error); 102 | } 103 | } 104 | ); 105 | 106 | // Reset embedders configuration 107 | server.tool( 108 | "reset-embedders", 109 | "Reset the embedders configuration for an index", 110 | { 111 | indexUid: z.string().describe("Unique identifier of the index"), 112 | }, 113 | async ({ indexUid }) => { 114 | try { 115 | const response = await apiClient.delete(`/indexes/${indexUid}/settings/embedders`); 116 | return { 117 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 118 | }; 119 | } catch (error) { 120 | return createErrorResponse(error); 121 | } 122 | } 123 | ); 124 | 125 | // Perform vector search 126 | server.tool( 127 | "vector-search", 128 | "Perform a vector search in a Meilisearch index", 129 | { 130 | indexUid: z.string().describe("Unique identifier of the index"), 131 | vector: z.string().describe("JSON array representing the vector to search for"), 132 | limit: z.number().min(1).max(1000).optional().describe("Maximum number of results to return (default: 20)"), 133 | offset: z.number().min(0).optional().describe("Number of results to skip (default: 0)"), 134 | filter: z.string().optional().describe("Filter to apply (e.g., 'genre = horror AND year > 2020')"), 135 | embedder: z.string().optional().describe("Name of the embedder to use (if omitted, a 'vector' must be provided)"), 136 | attributes: z.array(z.string()).optional().describe("Attributes to include in the vector search"), 137 | query: z.string().optional().describe("Text query to search for (if using 'embedder' instead of 'vector')"), 138 | hybrid: z.boolean().optional().describe("Whether to perform a hybrid search (combining vector and text search)"), 139 | hybridRatio: z.number().min(0).max(1).optional().describe("Ratio of vector vs text search in hybrid search (0-1, default: 0.5)"), 140 | }, 141 | async ({ indexUid, vector, limit, offset, filter, embedder, attributes, query, hybrid, hybridRatio }) => { 142 | try { 143 | const searchParams: Record<string, any> = {}; 144 | 145 | // Add required vector parameter 146 | if (vector) { 147 | try { 148 | searchParams.vector = JSON.parse(vector); 149 | } catch (parseError) { 150 | return { 151 | isError: true, 152 | content: [{ type: "text", text: "Vector must be a valid JSON array" }], 153 | }; 154 | } 155 | } 156 | 157 | // Add embedder parameters 158 | if (embedder) { 159 | searchParams.embedder = embedder; 160 | 161 | if (query !== undefined) { 162 | searchParams.q = query; 163 | } 164 | } 165 | 166 | // Ensure we have either vector or (embedder + query) 167 | if (!vector && (!embedder || query === undefined)) { 168 | return { 169 | isError: true, 170 | content: [{ type: "text", text: "Either 'vector' or both 'embedder' and 'query' must be provided" }], 171 | }; 172 | } 173 | 174 | // Add optional parameters 175 | if (limit !== undefined) searchParams.limit = limit; 176 | if (offset !== undefined) searchParams.offset = offset; 177 | if (filter) searchParams.filter = filter; 178 | if (attributes?.length) searchParams.attributes = attributes; 179 | if (hybrid !== undefined) searchParams.hybrid = hybrid; 180 | if (hybridRatio !== undefined) searchParams.hybridRatio = hybridRatio; 181 | 182 | const response = await apiClient.post(`/indexes/${indexUid}/search`, searchParams); 183 | return { 184 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 185 | }; 186 | } catch (error) { 187 | return createErrorResponse(error); 188 | } 189 | } 190 | ); 191 | }; 192 | 193 | export default registerVectorTools; 194 | ``` -------------------------------------------------------------------------------- /src/tools/document-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch Document Management Tools 9 | * 10 | * This module implements MCP tools for managing documents in Meilisearch indexes. 11 | */ 12 | 13 | // Define types for document parameters 14 | interface GetDocumentsParams { 15 | indexUid: string; 16 | limit?: number; 17 | offset?: number; 18 | fields?: string[]; 19 | filter?: string; 20 | } 21 | 22 | interface GetDocumentParams { 23 | indexUid: string; 24 | documentId: string; 25 | fields?: string[]; 26 | } 27 | 28 | interface AddDocumentsParams { 29 | indexUid: string; 30 | documents: string; 31 | primaryKey?: string; 32 | } 33 | 34 | interface UpdateDocumentsParams { 35 | indexUid: string; 36 | documents: string; 37 | primaryKey?: string; 38 | } 39 | 40 | interface DeleteDocumentParams { 41 | indexUid: string; 42 | documentId: string; 43 | } 44 | 45 | interface DeleteDocumentsParams { 46 | indexUid: string; 47 | documentIds: string; 48 | } 49 | 50 | interface DeleteAllDocumentsParams { 51 | indexUid: string; 52 | } 53 | 54 | /** 55 | * Register document management tools with the MCP server 56 | * 57 | * @param server - The MCP server instance 58 | */ 59 | export const registerDocumentTools = (server: McpServer) => { 60 | // Get documents from an index 61 | server.tool( 62 | 'get-documents', 63 | 'Get documents from a Meilisearch index', 64 | { 65 | indexUid: z.string().describe('Unique identifier of the index'), 66 | limit: z.number().min(1).max(1000).optional().describe('Maximum number of documents to return (default: 20)'), 67 | offset: z.number().min(0).optional().describe('Number of documents to skip (default: 0)'), 68 | fields: z.array(z.string()).optional().describe('Fields to return in the documents'), 69 | filter: z.string().optional().describe('Filter query to apply'), 70 | }, 71 | async ({ indexUid, limit, offset, fields, filter }: GetDocumentsParams) => { 72 | try { 73 | const response = await apiClient.get(`/indexes/${indexUid}/documents`, { 74 | params: { 75 | limit, 76 | offset, 77 | fields: fields?.join(','), 78 | filter, 79 | }, 80 | }); 81 | return { 82 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 83 | }; 84 | } catch (error) { 85 | return createErrorResponse(error); 86 | } 87 | } 88 | ); 89 | 90 | // Get a single document by ID 91 | server.tool( 92 | 'get-document', 93 | 'Get a document by its ID from a Meilisearch index', 94 | { 95 | indexUid: z.string().describe('Unique identifier of the index'), 96 | documentId: z.string().describe('ID of the document to retrieve'), 97 | fields: z.array(z.string()).optional().describe('Fields to return in the document'), 98 | }, 99 | async ({ indexUid, documentId, fields }: GetDocumentParams) => { 100 | try { 101 | const response = await apiClient.get(`/indexes/${indexUid}/documents/${documentId}`, { 102 | params: { 103 | fields: fields?.join(','), 104 | }, 105 | }); 106 | return { 107 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 108 | }; 109 | } catch (error) { 110 | return createErrorResponse(error); 111 | } 112 | } 113 | ); 114 | 115 | // Add documents to an index 116 | server.tool( 117 | 'add-documents', 118 | 'Add documents to a Meilisearch index', 119 | { 120 | indexUid: z.string().describe('Unique identifier of the index'), 121 | documents: z.string().describe('JSON array of documents to add'), 122 | primaryKey: z.string().optional().describe('Primary key for the documents'), 123 | }, 124 | async ({ indexUid, documents, primaryKey }: AddDocumentsParams) => { 125 | try { 126 | // Parse the documents string to ensure it's valid JSON 127 | const parsedDocuments = JSON.parse(documents); 128 | 129 | // Ensure documents is an array 130 | if (!Array.isArray(parsedDocuments)) { 131 | return { 132 | isError: true, 133 | content: [{ type: 'text', text: 'Documents must be a JSON array' }], 134 | }; 135 | } 136 | 137 | const params: Record<string, string> = {}; 138 | if (primaryKey) { 139 | params.primaryKey = primaryKey; 140 | } 141 | 142 | const response = await apiClient.post(`/indexes/${indexUid}/documents`, parsedDocuments, { 143 | params, 144 | }); 145 | return { 146 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 147 | }; 148 | } catch (error) { 149 | return createErrorResponse(error); 150 | } 151 | } 152 | ); 153 | 154 | // Update documents in an index 155 | server.tool( 156 | 'update-documents', 157 | 'Update documents in a Meilisearch index', 158 | { 159 | indexUid: z.string().describe('Unique identifier of the index'), 160 | documents: z.string().describe('JSON array of documents to update'), 161 | primaryKey: z.string().optional().describe('Primary key for the documents'), 162 | }, 163 | async ({ indexUid, documents, primaryKey }: UpdateDocumentsParams) => { 164 | try { 165 | // Parse the documents string to ensure it's valid JSON 166 | const parsedDocuments = JSON.parse(documents); 167 | 168 | // Ensure documents is an array 169 | if (!Array.isArray(parsedDocuments)) { 170 | return { 171 | isError: true, 172 | content: [{ type: 'text', text: 'Documents must be a JSON array' }], 173 | }; 174 | } 175 | 176 | const params: Record<string, string> = {}; 177 | if (primaryKey) { 178 | params.primaryKey = primaryKey; 179 | } 180 | 181 | const response = await apiClient.put(`/indexes/${indexUid}/documents`, parsedDocuments, { 182 | params, 183 | }); 184 | return { 185 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 186 | }; 187 | } catch (error) { 188 | return createErrorResponse(error); 189 | } 190 | } 191 | ); 192 | 193 | // Delete a document by ID 194 | server.tool( 195 | 'delete-document', 196 | 'Delete a document by its ID from a Meilisearch index', 197 | { 198 | indexUid: z.string().describe('Unique identifier of the index'), 199 | documentId: z.string().describe('ID of the document to delete'), 200 | }, 201 | async ({ indexUid, documentId }: DeleteDocumentParams) => { 202 | try { 203 | const response = await apiClient.delete(`/indexes/${indexUid}/documents/${documentId}`); 204 | return { 205 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 206 | }; 207 | } catch (error) { 208 | return createErrorResponse(error); 209 | } 210 | } 211 | ); 212 | 213 | // Delete multiple documents by ID 214 | server.tool( 215 | 'delete-documents', 216 | 'Delete multiple documents by their IDs from a Meilisearch index', 217 | { 218 | indexUid: z.string().describe('Unique identifier of the index'), 219 | documentIds: z.string().describe('JSON array of document IDs to delete'), 220 | }, 221 | async ({ indexUid, documentIds }: DeleteDocumentsParams) => { 222 | try { 223 | // Parse the document IDs string to ensure it's valid JSON 224 | const parsedDocumentIds = JSON.parse(documentIds); 225 | 226 | // Ensure document IDs is an array 227 | if (!Array.isArray(parsedDocumentIds)) { 228 | return { 229 | isError: true, 230 | content: [{ type: 'text', text: 'Document IDs must be a JSON array' }], 231 | }; 232 | } 233 | 234 | const response = await apiClient.post(`/indexes/${indexUid}/documents/delete-batch`, parsedDocumentIds); 235 | return { 236 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 237 | }; 238 | } catch (error) { 239 | return createErrorResponse(error); 240 | } 241 | } 242 | ); 243 | 244 | // Delete all documents in an index 245 | server.tool( 246 | 'delete-all-documents', 247 | 'Delete all documents in a Meilisearch index', 248 | { 249 | indexUid: z.string().describe('Unique identifier of the index'), 250 | }, 251 | async ({ indexUid }: DeleteAllDocumentsParams) => { 252 | try { 253 | const response = await apiClient.delete(`/indexes/${indexUid}/documents`); 254 | return { 255 | content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], 256 | }; 257 | } catch (error) { 258 | return createErrorResponse(error); 259 | } 260 | } 261 | ); 262 | }; 263 | 264 | export default registerDocumentTools; 265 | ``` -------------------------------------------------------------------------------- /src/tools/settings-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | import apiClient from '../utils/api-client.js'; 5 | import { createErrorResponse } from '../utils/error-handler.js'; 6 | 7 | /** 8 | * Meilisearch Settings Management Tools 9 | * 10 | * This module implements MCP tools for managing index settings in Meilisearch. 11 | */ 12 | 13 | /** 14 | * Register settings management tools with the MCP server 15 | * 16 | * @param server - The MCP server instance 17 | */ 18 | export const registerSettingsTools = (server: McpServer) => { 19 | // Get all settings 20 | server.tool( 21 | "get-settings", 22 | "Get all settings for a Meilisearch index", 23 | { 24 | indexUid: z.string().describe("Unique identifier of the index"), 25 | }, 26 | async ({ indexUid }) => { 27 | try { 28 | const response = await apiClient.get(`/indexes/${indexUid}/settings`); 29 | return { 30 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 31 | }; 32 | } catch (error) { 33 | return createErrorResponse(error); 34 | } 35 | } 36 | ); 37 | 38 | // Update settings 39 | server.tool( 40 | "update-settings", 41 | "Update settings for a Meilisearch index", 42 | { 43 | indexUid: z.string().describe("Unique identifier of the index"), 44 | settings: z.string().describe("JSON object containing settings to update"), 45 | }, 46 | async ({ indexUid, settings }) => { 47 | try { 48 | // Parse the settings string to ensure it's valid JSON 49 | const parsedSettings = JSON.parse(settings); 50 | 51 | // Ensure settings is an object 52 | if (typeof parsedSettings !== 'object' || parsedSettings === null || Array.isArray(parsedSettings)) { 53 | return { 54 | isError: true, 55 | content: [{ type: "text", text: "Settings must be a JSON object" }], 56 | }; 57 | } 58 | 59 | const response = await apiClient.patch(`/indexes/${indexUid}/settings`, parsedSettings); 60 | return { 61 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 62 | }; 63 | } catch (error) { 64 | return createErrorResponse(error); 65 | } 66 | } 67 | ); 68 | 69 | // Reset settings 70 | server.tool( 71 | "reset-settings", 72 | "Reset all settings for a Meilisearch index to their default values", 73 | { 74 | indexUid: z.string().describe("Unique identifier of the index"), 75 | }, 76 | async ({ indexUid }) => { 77 | try { 78 | const response = await apiClient.delete(`/indexes/${indexUid}/settings`); 79 | return { 80 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 81 | }; 82 | } catch (error) { 83 | return createErrorResponse(error); 84 | } 85 | } 86 | ); 87 | 88 | // Get specific settings 89 | const specificSettingsTools = [ 90 | { 91 | name: "get-searchable-attributes", 92 | endpoint: "searchable-attributes", 93 | description: "Get the searchable attributes setting", 94 | }, 95 | { 96 | name: "get-displayed-attributes", 97 | endpoint: "displayed-attributes", 98 | description: "Get the displayed attributes setting", 99 | }, 100 | { 101 | name: "get-filterable-attributes", 102 | endpoint: "filterable-attributes", 103 | description: "Get the filterable attributes setting", 104 | }, 105 | { 106 | name: "get-sortable-attributes", 107 | endpoint: "sortable-attributes", 108 | description: "Get the sortable attributes setting", 109 | }, 110 | { 111 | name: "get-ranking-rules", 112 | endpoint: "ranking-rules", 113 | description: "Get the ranking rules setting", 114 | }, 115 | { 116 | name: "get-stop-words", 117 | endpoint: "stop-words", 118 | description: "Get the stop words setting", 119 | }, 120 | { 121 | name: "get-synonyms", 122 | endpoint: "synonyms", 123 | description: "Get the synonyms setting", 124 | }, 125 | { 126 | name: "get-distinct-attribute", 127 | endpoint: "distinct-attribute", 128 | description: "Get the distinct attribute setting", 129 | }, 130 | { 131 | name: "get-typo-tolerance", 132 | endpoint: "typo-tolerance", 133 | description: "Get the typo tolerance setting", 134 | }, 135 | { 136 | name: "get-faceting", 137 | endpoint: "faceting", 138 | description: "Get the faceting setting", 139 | }, 140 | { 141 | name: "get-pagination", 142 | endpoint: "pagination", 143 | description: "Get the pagination setting", 144 | }, 145 | ]; 146 | 147 | // Create a tool for each specific setting 148 | specificSettingsTools.forEach(({ name, endpoint, description }) => { 149 | server.tool( 150 | name, 151 | description, 152 | { 153 | indexUid: z.string().describe("Unique identifier of the index"), 154 | }, 155 | async ({ indexUid }) => { 156 | try { 157 | const response = await apiClient.get(`/indexes/${indexUid}/settings/${endpoint}`); 158 | return { 159 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 160 | }; 161 | } catch (error) { 162 | return createErrorResponse(error); 163 | } 164 | } 165 | ); 166 | }); 167 | 168 | // Update specific settings 169 | const updateSettingsTools = [ 170 | { 171 | name: "update-searchable-attributes", 172 | endpoint: "searchable-attributes", 173 | description: "Update the searchable attributes setting", 174 | }, 175 | { 176 | name: "update-displayed-attributes", 177 | endpoint: "displayed-attributes", 178 | description: "Update the displayed attributes setting", 179 | }, 180 | { 181 | name: "update-filterable-attributes", 182 | endpoint: "filterable-attributes", 183 | description: "Update the filterable attributes setting", 184 | }, 185 | { 186 | name: "update-sortable-attributes", 187 | endpoint: "sortable-attributes", 188 | description: "Update the sortable attributes setting", 189 | }, 190 | { 191 | name: "update-ranking-rules", 192 | endpoint: "ranking-rules", 193 | description: "Update the ranking rules setting", 194 | }, 195 | { 196 | name: "update-stop-words", 197 | endpoint: "stop-words", 198 | description: "Update the stop words setting", 199 | }, 200 | { 201 | name: "update-synonyms", 202 | endpoint: "synonyms", 203 | description: "Update the synonyms setting", 204 | }, 205 | { 206 | name: "update-distinct-attribute", 207 | endpoint: "distinct-attribute", 208 | description: "Update the distinct attribute setting", 209 | }, 210 | { 211 | name: "update-typo-tolerance", 212 | endpoint: "typo-tolerance", 213 | description: "Update the typo tolerance setting", 214 | }, 215 | { 216 | name: "update-faceting", 217 | endpoint: "faceting", 218 | description: "Update the faceting setting", 219 | }, 220 | { 221 | name: "update-pagination", 222 | endpoint: "pagination", 223 | description: "Update the pagination setting", 224 | }, 225 | ]; 226 | 227 | // Create an update tool for each specific setting 228 | updateSettingsTools.forEach(({ name, endpoint, description }) => { 229 | server.tool( 230 | name, 231 | description, 232 | { 233 | indexUid: z.string().describe("Unique identifier of the index"), 234 | value: z.string().describe("JSON value for the setting"), 235 | }, 236 | async ({ indexUid, value }) => { 237 | try { 238 | // Parse the value string to ensure it's valid JSON 239 | const parsedValue = JSON.parse(value); 240 | 241 | const response = await apiClient.put(`/indexes/${indexUid}/settings/${endpoint}`, parsedValue); 242 | return { 243 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 244 | }; 245 | } catch (error) { 246 | return createErrorResponse(error); 247 | } 248 | } 249 | ); 250 | }); 251 | 252 | // Reset specific settings 253 | const resetSettingsTools = [ 254 | { 255 | name: "reset-searchable-attributes", 256 | endpoint: "searchable-attributes", 257 | description: "Reset the searchable attributes setting to its default value", 258 | }, 259 | { 260 | name: "reset-displayed-attributes", 261 | endpoint: "displayed-attributes", 262 | description: "Reset the displayed attributes setting to its default value", 263 | }, 264 | { 265 | name: "reset-filterable-attributes", 266 | endpoint: "filterable-attributes", 267 | description: "Reset the filterable attributes setting to its default value", 268 | }, 269 | { 270 | name: "reset-sortable-attributes", 271 | endpoint: "sortable-attributes", 272 | description: "Reset the sortable attributes setting to its default value", 273 | }, 274 | { 275 | name: "reset-ranking-rules", 276 | endpoint: "ranking-rules", 277 | description: "Reset the ranking rules setting to its default value", 278 | }, 279 | { 280 | name: "reset-stop-words", 281 | endpoint: "stop-words", 282 | description: "Reset the stop words setting to its default value", 283 | }, 284 | { 285 | name: "reset-synonyms", 286 | endpoint: "synonyms", 287 | description: "Reset the synonyms setting to its default value", 288 | }, 289 | { 290 | name: "reset-distinct-attribute", 291 | endpoint: "distinct-attribute", 292 | description: "Reset the distinct attribute setting to its default value", 293 | }, 294 | { 295 | name: "reset-typo-tolerance", 296 | endpoint: "typo-tolerance", 297 | description: "Reset the typo tolerance setting to its default value", 298 | }, 299 | { 300 | name: "reset-faceting", 301 | endpoint: "faceting", 302 | description: "Reset the faceting setting to its default value", 303 | }, 304 | { 305 | name: "reset-pagination", 306 | endpoint: "pagination", 307 | description: "Reset the pagination setting to its default value", 308 | }, 309 | ]; 310 | 311 | // Create a reset tool for each specific setting 312 | resetSettingsTools.forEach(({ name, endpoint, description }) => { 313 | server.tool( 314 | name, 315 | description, 316 | { 317 | indexUid: z.string().describe("Unique identifier of the index"), 318 | }, 319 | async ({ indexUid }) => { 320 | try { 321 | const response = await apiClient.delete(`/indexes/${indexUid}/settings/${endpoint}`); 322 | return { 323 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }], 324 | }; 325 | } catch (error) { 326 | return createErrorResponse(error); 327 | } 328 | } 329 | ); 330 | }); 331 | }; 332 | 333 | export default registerSettingsTools; 334 | ```