mcp-meilisearch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,190 @@
1
+ import { z } from "zod";
2
+ import apiClient from "../utils/api-handler.js";
3
+ import { createErrorResponse } from "../utils/error-handler.js";
4
+ /**
5
+ * Register task management tools with the MCP server
6
+ *
7
+ * @param server - The MCP server instance
8
+ */
9
+ export const registerTaskTools = (server) => {
10
+ // Get all tasks
11
+ server.tool("list-tasks", "List tasks with optional filtering", {
12
+ limit: z
13
+ .number()
14
+ .min(0)
15
+ .optional()
16
+ .describe("Maximum number of tasks to return"),
17
+ from: z
18
+ .number()
19
+ .min(0)
20
+ .optional()
21
+ .describe("Task uid from which to start fetching"),
22
+ statuses: z
23
+ .array(z.enum(["enqueued", "processing", "succeeded", "failed", "canceled"]))
24
+ .optional()
25
+ .describe("Statuses of tasks to return"),
26
+ types: z
27
+ .array(z.enum([
28
+ "indexCreation",
29
+ "indexUpdate",
30
+ "indexDeletion",
31
+ "documentAddition",
32
+ "documentUpdate",
33
+ "documentDeletion",
34
+ "settingsUpdate",
35
+ "dumpCreation",
36
+ "taskCancelation",
37
+ ]))
38
+ .optional()
39
+ .describe("Types of tasks to return"),
40
+ indexUids: z
41
+ .array(z.string())
42
+ .optional()
43
+ .describe("UIDs of the indexes on which tasks were performed"),
44
+ uids: z
45
+ .array(z.number())
46
+ .optional()
47
+ .describe("UIDs of specific tasks to return"),
48
+ }, async ({ limit, from, statuses, types, indexUids, uids, }) => {
49
+ try {
50
+ const params = {};
51
+ if (limit !== undefined)
52
+ params.limit = limit;
53
+ if (from !== undefined)
54
+ params.from = from;
55
+ if (statuses && statuses.length > 0)
56
+ params.statuses = statuses.join(",");
57
+ if (types && types.length > 0)
58
+ params.types = types.join(",");
59
+ if (indexUids && indexUids.length > 0)
60
+ params.indexUids = indexUids.join(",");
61
+ if (uids && uids.length > 0)
62
+ params.uids = uids.join(",");
63
+ const response = await apiClient.get("/tasks", { params });
64
+ return {
65
+ content: [
66
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
67
+ ],
68
+ };
69
+ }
70
+ catch (error) {
71
+ return createErrorResponse(error);
72
+ }
73
+ });
74
+ // Get a specific task
75
+ server.tool("get-task", "Get information about a specific task", {
76
+ taskUid: z.number().describe("Unique identifier of the task"),
77
+ }, async ({ taskUid }) => {
78
+ try {
79
+ const response = await apiClient.get(`/tasks/${taskUid}`);
80
+ return {
81
+ content: [
82
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
83
+ ],
84
+ };
85
+ }
86
+ catch (error) {
87
+ return createErrorResponse(error);
88
+ }
89
+ });
90
+ // Cancel tasks
91
+ server.tool("cancel-tasks", "Cancel tasks based on provided filters", {
92
+ statuses: z
93
+ .array(z.enum(["enqueued", "processing"]))
94
+ .optional()
95
+ .describe("Statuses of tasks to cancel"),
96
+ types: z
97
+ .array(z.enum([
98
+ "indexCreation",
99
+ "indexUpdate",
100
+ "indexDeletion",
101
+ "documentAddition",
102
+ "documentUpdate",
103
+ "documentDeletion",
104
+ "settingsUpdate",
105
+ "dumpCreation",
106
+ "taskCancelation",
107
+ ]))
108
+ .optional()
109
+ .describe("Types of tasks to cancel"),
110
+ indexUids: z
111
+ .array(z.string())
112
+ .optional()
113
+ .describe("UIDs of the indexes on which tasks to cancel were performed"),
114
+ uids: z
115
+ .array(z.number())
116
+ .optional()
117
+ .describe("UIDs of the tasks to cancel"),
118
+ }, async ({ statuses, types, indexUids, uids }) => {
119
+ try {
120
+ const body = {};
121
+ if (statuses && statuses.length > 0)
122
+ body.statuses = statuses;
123
+ if (types && types.length > 0)
124
+ body.types = types;
125
+ if (indexUids && indexUids.length > 0)
126
+ body.indexUids = indexUids;
127
+ if (uids && uids.length > 0)
128
+ body.uids = uids;
129
+ const response = await apiClient.post("/tasks/cancel", body);
130
+ return {
131
+ content: [
132
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
133
+ ],
134
+ };
135
+ }
136
+ catch (error) {
137
+ return createErrorResponse(error);
138
+ }
139
+ });
140
+ // Wait for a task to complete
141
+ server.tool("wait-for-task", "Wait for a specific task to complete", {
142
+ taskUid: z.number().describe("Unique identifier of the task to wait for"),
143
+ timeoutMs: z
144
+ .number()
145
+ .min(0)
146
+ .optional()
147
+ .describe("Maximum time to wait in milliseconds (default: 5000)"),
148
+ intervalMs: z
149
+ .number()
150
+ .min(100)
151
+ .optional()
152
+ .describe("Polling interval in milliseconds (default: 500)"),
153
+ }, async ({ taskUid, timeoutMs = 5000, intervalMs = 500, }) => {
154
+ try {
155
+ const startTime = Date.now();
156
+ let taskCompleted = false;
157
+ let taskData = null;
158
+ while (!taskCompleted && Date.now() - startTime < timeoutMs) {
159
+ // Fetch the current task status
160
+ const response = await apiClient.get(`/tasks/${taskUid}`);
161
+ taskData = response.data;
162
+ // Check if the task has completed
163
+ if (["succeeded", "failed", "canceled"].includes(taskData.status)) {
164
+ taskCompleted = true;
165
+ }
166
+ else {
167
+ // Wait for the polling interval
168
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
169
+ }
170
+ }
171
+ if (!taskCompleted) {
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `Task ${taskUid} did not complete within the timeout period of ${timeoutMs}ms`,
177
+ },
178
+ ],
179
+ };
180
+ }
181
+ return {
182
+ content: [{ type: "text", text: JSON.stringify(taskData, null, 2) }],
183
+ };
184
+ }
185
+ catch (error) {
186
+ return createErrorResponse(error);
187
+ }
188
+ });
189
+ };
190
+ export default registerTaskTools;
@@ -0,0 +1,214 @@
1
+ import { z } from "zod";
2
+ import apiClient from "../utils/api-handler.js";
3
+ import { createErrorResponse } from "../utils/error-handler.js";
4
+ /**
5
+ * Meilisearch Vector Search Tools
6
+ *
7
+ * This module implements MCP tools for vector search capabilities in Meilisearch.
8
+ * Note: Vector search is an experimental feature in Meilisearch.
9
+ */
10
+ /**
11
+ * Register vector search tools with the MCP server
12
+ *
13
+ * @param server - The MCP server instance
14
+ */
15
+ export const registerVectorTools = (server) => {
16
+ // Enable vector search experimental feature
17
+ server.tool("enable-vector-search", "Enable the vector search experimental feature in Meilisearch", {}, async () => {
18
+ try {
19
+ const response = await apiClient.post("/experimental-features", {
20
+ vectorStore: true,
21
+ });
22
+ return {
23
+ content: [
24
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
25
+ ],
26
+ };
27
+ }
28
+ catch (error) {
29
+ return createErrorResponse(error);
30
+ }
31
+ });
32
+ // Get experimental features status
33
+ server.tool("get-experimental-features", "Get the status of experimental features in Meilisearch", {}, async () => {
34
+ try {
35
+ const response = await apiClient.get("/experimental-features");
36
+ return {
37
+ content: [
38
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
39
+ ],
40
+ };
41
+ }
42
+ catch (error) {
43
+ return createErrorResponse(error);
44
+ }
45
+ });
46
+ // Update embedders configuration
47
+ server.tool("update-embedders", "Configure embedders for vector search", {
48
+ indexUid: z.string().describe("Unique identifier of the index"),
49
+ embedders: z
50
+ .string()
51
+ .describe("JSON object containing embedder configurations"),
52
+ }, async ({ indexUid, embedders }) => {
53
+ try {
54
+ // Parse the embedders string to ensure it's valid JSON
55
+ const parsedEmbedders = JSON.parse(embedders);
56
+ // Ensure embedders is an object
57
+ if (typeof parsedEmbedders !== "object" ||
58
+ parsedEmbedders === null ||
59
+ Array.isArray(parsedEmbedders)) {
60
+ return {
61
+ isError: true,
62
+ content: [
63
+ { type: "text", text: "Embedders must be a JSON object" },
64
+ ],
65
+ };
66
+ }
67
+ const response = await apiClient.patch(`/indexes/${indexUid}/settings/embedders`, parsedEmbedders);
68
+ return {
69
+ content: [
70
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
71
+ ],
72
+ };
73
+ }
74
+ catch (error) {
75
+ return createErrorResponse(error);
76
+ }
77
+ });
78
+ // Get embedders configuration
79
+ server.tool("get-embedders", "Get the embedders configuration for an index", {
80
+ indexUid: z.string().describe("Unique identifier of the index"),
81
+ }, async ({ indexUid }) => {
82
+ try {
83
+ const response = await apiClient.get(`/indexes/${indexUid}/settings/embedders`);
84
+ return {
85
+ content: [
86
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
87
+ ],
88
+ };
89
+ }
90
+ catch (error) {
91
+ return createErrorResponse(error);
92
+ }
93
+ });
94
+ // Reset embedders configuration
95
+ server.tool("reset-embedders", "Reset the embedders configuration for an index", {
96
+ indexUid: z.string().describe("Unique identifier of the index"),
97
+ }, async ({ indexUid }) => {
98
+ try {
99
+ const response = await apiClient.delete(`/indexes/${indexUid}/settings/embedders`);
100
+ return {
101
+ content: [
102
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
103
+ ],
104
+ };
105
+ }
106
+ catch (error) {
107
+ return createErrorResponse(error);
108
+ }
109
+ });
110
+ // Perform vector search
111
+ server.tool("vector-search", "Perform a vector search in a Meilisearch index", {
112
+ indexUid: z.string().describe("Unique identifier of the index"),
113
+ vector: z
114
+ .string()
115
+ .describe("JSON array representing the vector to search for"),
116
+ limit: z
117
+ .number()
118
+ .min(1)
119
+ .max(1000)
120
+ .optional()
121
+ .describe("Maximum number of results to return (default: 20)"),
122
+ offset: z
123
+ .number()
124
+ .min(0)
125
+ .optional()
126
+ .describe("Number of results to skip (default: 0)"),
127
+ filter: z
128
+ .string()
129
+ .optional()
130
+ .describe("Filter to apply (e.g., 'genre = horror AND year > 2020')"),
131
+ embedder: z
132
+ .string()
133
+ .optional()
134
+ .describe("Name of the embedder to use (if omitted, a 'vector' must be provided)"),
135
+ attributes: z
136
+ .array(z.string())
137
+ .optional()
138
+ .describe("Attributes to include in the vector search"),
139
+ query: z
140
+ .string()
141
+ .optional()
142
+ .describe("Text query to search for (if using 'embedder' instead of 'vector')"),
143
+ hybrid: z
144
+ .boolean()
145
+ .optional()
146
+ .describe("Whether to perform a hybrid search (combining vector and text search)"),
147
+ hybridRatio: z
148
+ .number()
149
+ .min(0)
150
+ .max(1)
151
+ .optional()
152
+ .describe("Ratio of vector vs text search in hybrid search (0-1, default: 0.5)"),
153
+ }, async ({ indexUid, vector, limit, offset, filter, embedder, attributes, query, hybrid, hybridRatio, }) => {
154
+ try {
155
+ const searchParams = {};
156
+ // Add required vector parameter
157
+ if (vector) {
158
+ try {
159
+ searchParams.vector = JSON.parse(vector);
160
+ }
161
+ catch (parseError) {
162
+ return {
163
+ isError: true,
164
+ content: [
165
+ { type: "text", text: "Vector must be a valid JSON array" },
166
+ ],
167
+ };
168
+ }
169
+ }
170
+ // Add embedder parameters
171
+ if (embedder) {
172
+ searchParams.embedder = embedder;
173
+ if (query !== undefined) {
174
+ searchParams.q = query;
175
+ }
176
+ }
177
+ // Ensure we have either vector or (embedder + query)
178
+ if (!vector && (!embedder || query === undefined)) {
179
+ return {
180
+ isError: true,
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: "Either 'vector' or both 'embedder' and 'query' must be provided",
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ // Add optional parameters
190
+ if (limit !== undefined)
191
+ searchParams.limit = limit;
192
+ if (offset !== undefined)
193
+ searchParams.offset = offset;
194
+ if (filter)
195
+ searchParams.filter = filter;
196
+ if (attributes?.length)
197
+ searchParams.attributes = attributes;
198
+ if (hybrid !== undefined)
199
+ searchParams.hybrid = hybrid;
200
+ if (hybridRatio !== undefined)
201
+ searchParams.hybridRatio = hybridRatio;
202
+ const response = await apiClient.post(`/indexes/${indexUid}/search`, searchParams);
203
+ return {
204
+ content: [
205
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
206
+ ],
207
+ };
208
+ }
209
+ catch (error) {
210
+ return createErrorResponse(error);
211
+ }
212
+ });
213
+ };
214
+ export default registerVectorTools;
@@ -0,0 +1,33 @@
1
+ import axios from 'axios';
2
+ import config from '../config.js';
3
+ /**
4
+ * Meilisearch API client
5
+ *
6
+ * This module provides a configured Axios instance for making requests to the Meilisearch API.
7
+ */
8
+ /**
9
+ * Creates a configured Axios instance for Meilisearch API requests
10
+ *
11
+ * @returns An Axios instance with base configuration
12
+ */
13
+ export const createApiClient = () => {
14
+ const instance = axios.create({
15
+ baseURL: config.host,
16
+ headers: {
17
+ Authorization: `Bearer ${config.apiKey}`,
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ timeout: config.timeout,
21
+ });
22
+ return {
23
+ get: (url, config) => instance.get(url, config),
24
+ post: (url, data, config) => instance.post(url, data, config),
25
+ put: (url, data, config) => instance.put(url, data, config),
26
+ patch: (url, data, config) => instance.patch(url, data, config),
27
+ delete: (url, config) => instance.delete(url, config),
28
+ };
29
+ };
30
+ // Create and export a singleton instance of the API client
31
+ export const apiClient = createApiClient();
32
+ // Re-export for direct use
33
+ export default apiClient;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Error handling utilities for Meilisearch API responses
3
+ */
4
+ /**
5
+ * Formats Meilisearch API errors for consistent error messaging
6
+ *
7
+ * @param error - The error from the API request
8
+ * @returns A formatted error message
9
+ */
10
+ export const handleApiError = (error) => {
11
+ // If it's an Axios error with a response
12
+ if (error.isAxiosError && error.response) {
13
+ const { status, data } = error.response;
14
+ // Return formatted error with status code and response data
15
+ return `Meilisearch API error (${status}): ${JSON.stringify(data)}`;
16
+ }
17
+ // If it's a network error or other error
18
+ return `Error connecting to Meilisearch: ${error.message}`;
19
+ };
20
+ /**
21
+ * Creates a standardized error response object for MCP tools
22
+ *
23
+ * @param error - The error from the API request
24
+ * @returns An MCP tool response object with error flag
25
+ */
26
+ export const createErrorResponse = (error) => {
27
+ return {
28
+ isError: true,
29
+ content: [{ type: "text", text: handleApiError(error) }],
30
+ };
31
+ };
32
+ export default {
33
+ handleApiError,
34
+ createErrorResponse,
35
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "mcp-meilisearch",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol (MCP) implementation for Meilisearch",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "workspaces": [
13
+ "client"
14
+ ],
15
+ "scripts": {
16
+ "build:http": "tsc && chmod 755 dist/http.js",
17
+ "build:stdio": "tsc && chmod 755 dist/stdio.js",
18
+ "server": "npm run build:stdio && node --env-file=.env dist/stdio.js",
19
+ "client": "npm run build:http && node --env-file=.env dist/http.js & npm run preview --workspace=client",
20
+ "prepare": "tsc",
21
+ "prepublishOnly": "npm run build:http && npm run build:stdio"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.10.1",
25
+ "axios": "^1.9.0",
26
+ "express": "^5.1.0",
27
+ "vue": "^3.5.13",
28
+ "zod": "^3.24.4"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^5.0.1",
32
+ "@types/node": "^22.14.0",
33
+ "@vitejs/plugin-vue": "^5.2.3",
34
+ "@vue/tsconfig": "^0.7.0",
35
+ "typescript": "^5.8.3",
36
+ "vite": "^6.3.5"
37
+ },
38
+ "engines": {
39
+ "node": ">=20.12.2",
40
+ "npm": ">=10.5.0"
41
+ }
42
+ }