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