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,208 @@
1
+ import { z } from "zod";
2
+ import apiClient from "../utils/api-handler.js";
3
+ import { createErrorResponse } from "../utils/error-handler.js";
4
+ /**
5
+ * Register document management tools with the MCP server
6
+ *
7
+ * @param server - The MCP server instance
8
+ */
9
+ export const registerDocumentTools = (server) => {
10
+ // Get documents from an index
11
+ server.tool("get-documents", "Get documents from a Meilisearch index", {
12
+ indexUid: z.string().describe("Unique identifier of the index"),
13
+ limit: z
14
+ .number()
15
+ .min(1)
16
+ .max(1000)
17
+ .optional()
18
+ .describe("Maximum number of documents to return (default: 20)"),
19
+ offset: z
20
+ .number()
21
+ .min(0)
22
+ .optional()
23
+ .describe("Number of documents to skip (default: 0)"),
24
+ fields: z
25
+ .array(z.string())
26
+ .optional()
27
+ .describe("Fields to return in the documents"),
28
+ filter: z.string().optional().describe("Filter query to apply"),
29
+ }, async ({ indexUid, limit, offset, fields, filter }) => {
30
+ try {
31
+ const response = await apiClient.get(`/indexes/${indexUid}/documents`, {
32
+ params: {
33
+ limit,
34
+ offset,
35
+ fields: fields?.join(","),
36
+ filter,
37
+ },
38
+ });
39
+ return {
40
+ content: [
41
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
42
+ ],
43
+ };
44
+ }
45
+ catch (error) {
46
+ return createErrorResponse(error);
47
+ }
48
+ });
49
+ // Get a single document by ID
50
+ server.tool("get-document", "Get a document by its ID from a Meilisearch index", {
51
+ indexUid: z.string().describe("Unique identifier of the index"),
52
+ documentId: z.string().describe("ID of the document to retrieve"),
53
+ fields: z
54
+ .array(z.string())
55
+ .optional()
56
+ .describe("Fields to return in the document"),
57
+ }, async ({ indexUid, documentId, fields }) => {
58
+ try {
59
+ const response = await apiClient.get(`/indexes/${indexUid}/documents/${documentId}`, {
60
+ params: {
61
+ fields: fields?.join(","),
62
+ },
63
+ });
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
+ // Add documents to an index
75
+ server.tool("add-documents", "Add documents to a Meilisearch index", {
76
+ indexUid: z.string().describe("Unique identifier of the index"),
77
+ documents: z.string().describe("JSON array of documents to add"),
78
+ primaryKey: z
79
+ .string()
80
+ .optional()
81
+ .describe("Primary key for the documents"),
82
+ }, async ({ indexUid, documents, primaryKey }) => {
83
+ try {
84
+ // Parse the documents string to ensure it's valid JSON
85
+ const parsedDocuments = JSON.parse(documents);
86
+ // Ensure documents is an array
87
+ if (!Array.isArray(parsedDocuments)) {
88
+ return {
89
+ isError: true,
90
+ content: [{ type: "text", text: "Documents must be a JSON array" }],
91
+ };
92
+ }
93
+ const params = {};
94
+ if (primaryKey) {
95
+ params.primaryKey = primaryKey;
96
+ }
97
+ const response = await apiClient.post(`/indexes/${indexUid}/documents`, parsedDocuments, {
98
+ params,
99
+ });
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
+ // Update documents in an index
111
+ server.tool("update-documents", "Update documents in a Meilisearch index", {
112
+ indexUid: z.string().describe("Unique identifier of the index"),
113
+ documents: z.string().describe("JSON array of documents to update"),
114
+ primaryKey: z
115
+ .string()
116
+ .optional()
117
+ .describe("Primary key for the documents"),
118
+ }, async ({ indexUid, documents, primaryKey }) => {
119
+ try {
120
+ // Parse the documents string to ensure it's valid JSON
121
+ const parsedDocuments = JSON.parse(documents);
122
+ // Ensure documents is an array
123
+ if (!Array.isArray(parsedDocuments)) {
124
+ return {
125
+ isError: true,
126
+ content: [{ type: "text", text: "Documents must be a JSON array" }],
127
+ };
128
+ }
129
+ const params = {};
130
+ if (primaryKey) {
131
+ params.primaryKey = primaryKey;
132
+ }
133
+ const response = await apiClient.put(`/indexes/${indexUid}/documents`, parsedDocuments, {
134
+ params,
135
+ });
136
+ return {
137
+ content: [
138
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
139
+ ],
140
+ };
141
+ }
142
+ catch (error) {
143
+ return createErrorResponse(error);
144
+ }
145
+ });
146
+ // Delete a document by ID
147
+ server.tool("delete-document", "Delete a document by its ID from a Meilisearch index", {
148
+ indexUid: z.string().describe("Unique identifier of the index"),
149
+ documentId: z.string().describe("ID of the document to delete"),
150
+ }, async ({ indexUid, documentId }) => {
151
+ try {
152
+ const response = await apiClient.delete(`/indexes/${indexUid}/documents/${documentId}`);
153
+ return {
154
+ content: [
155
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
156
+ ],
157
+ };
158
+ }
159
+ catch (error) {
160
+ return createErrorResponse(error);
161
+ }
162
+ });
163
+ // Delete multiple documents by ID
164
+ server.tool("delete-documents", "Delete multiple documents by their IDs from a Meilisearch index", {
165
+ indexUid: z.string().describe("Unique identifier of the index"),
166
+ documentIds: z.string().describe("JSON array of document IDs to delete"),
167
+ }, async ({ indexUid, documentIds }) => {
168
+ try {
169
+ // Parse the document IDs string to ensure it's valid JSON
170
+ const parsedDocumentIds = JSON.parse(documentIds);
171
+ // Ensure document IDs is an array
172
+ if (!Array.isArray(parsedDocumentIds)) {
173
+ return {
174
+ isError: true,
175
+ content: [
176
+ { type: "text", text: "Document IDs must be a JSON array" },
177
+ ],
178
+ };
179
+ }
180
+ const response = await apiClient.post(`/indexes/${indexUid}/documents/delete-batch`, parsedDocumentIds);
181
+ return {
182
+ content: [
183
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
184
+ ],
185
+ };
186
+ }
187
+ catch (error) {
188
+ return createErrorResponse(error);
189
+ }
190
+ });
191
+ // Delete all documents in an index
192
+ server.tool("delete-all-documents", "Delete all documents in a Meilisearch index", {
193
+ indexUid: z.string().describe("Unique identifier of the index"),
194
+ }, async ({ indexUid }) => {
195
+ try {
196
+ const response = await apiClient.delete(`/indexes/${indexUid}/documents`);
197
+ return {
198
+ content: [
199
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
200
+ ],
201
+ };
202
+ }
203
+ catch (error) {
204
+ return createErrorResponse(error);
205
+ }
206
+ });
207
+ };
208
+ export default registerDocumentTools;
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ import apiClient from "../utils/api-handler.js";
3
+ import { createErrorResponse } from "../utils/error-handler.js";
4
+ /**
5
+ * Register index management tools with the MCP server
6
+ *
7
+ * @param server - The MCP server instance
8
+ */
9
+ export const registerIndexTools = (server) => {
10
+ // List all indexes
11
+ server.tool("list-indexes", "List all indexes in the Meilisearch instance", {
12
+ limit: z
13
+ .number()
14
+ .min(1)
15
+ .max(100)
16
+ .optional()
17
+ .describe("Maximum number of indexes to return"),
18
+ offset: z
19
+ .number()
20
+ .min(0)
21
+ .optional()
22
+ .describe("Number of indexes to skip"),
23
+ }, async ({ limit, offset }) => {
24
+ try {
25
+ const response = await apiClient.get("/indexes", {
26
+ params: {
27
+ limit,
28
+ offset,
29
+ },
30
+ });
31
+ return {
32
+ content: [
33
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
34
+ ],
35
+ };
36
+ }
37
+ catch (error) {
38
+ return createErrorResponse(error);
39
+ }
40
+ });
41
+ // Get index information
42
+ server.tool("get-index", "Get information about a specific Meilisearch index", {
43
+ indexUid: z.string().describe("Unique identifier of the index"),
44
+ }, async ({ indexUid }) => {
45
+ try {
46
+ const response = await apiClient.get(`/indexes/${indexUid}`);
47
+ return {
48
+ content: [
49
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
50
+ ],
51
+ };
52
+ }
53
+ catch (error) {
54
+ return createErrorResponse(error);
55
+ }
56
+ });
57
+ // Create a new index
58
+ server.tool("create-index", "Create a new Meilisearch index", {
59
+ indexUid: z.string().describe("Unique identifier for the new index"),
60
+ primaryKey: z.string().optional().describe("Primary key for the index"),
61
+ }, async ({ indexUid, primaryKey }) => {
62
+ try {
63
+ const response = await apiClient.post("/indexes", {
64
+ uid: indexUid,
65
+ primaryKey,
66
+ });
67
+ return {
68
+ content: [
69
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
70
+ ],
71
+ };
72
+ }
73
+ catch (error) {
74
+ return createErrorResponse(error);
75
+ }
76
+ });
77
+ // Update an index
78
+ server.tool("update-index", "Update a Meilisearch index (currently only supports updating the primary key)", {
79
+ indexUid: z.string().describe("Unique identifier of the index"),
80
+ primaryKey: z.string().describe("New primary key for the index"),
81
+ }, async ({ indexUid, primaryKey }) => {
82
+ try {
83
+ const response = await apiClient.patch(`/indexes/${indexUid}`, {
84
+ primaryKey,
85
+ });
86
+ return {
87
+ content: [
88
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
89
+ ],
90
+ };
91
+ }
92
+ catch (error) {
93
+ return createErrorResponse(error);
94
+ }
95
+ });
96
+ // Delete an index
97
+ server.tool("delete-index", "Delete a Meilisearch index", {
98
+ indexUid: z.string().describe("Unique identifier of the index to delete"),
99
+ }, async ({ indexUid }) => {
100
+ try {
101
+ const response = await apiClient.delete(`/indexes/${indexUid}`);
102
+ return {
103
+ content: [
104
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
105
+ ],
106
+ };
107
+ }
108
+ catch (error) {
109
+ return createErrorResponse(error);
110
+ }
111
+ });
112
+ // Swap indexes
113
+ server.tool("swap-indexes", "Swap two or more indexes in Meilisearch", {
114
+ indexes: z
115
+ .string()
116
+ .describe('JSON array of index pairs to swap, e.g. [["movies", "movies_new"]]'),
117
+ }, async ({ indexes }) => {
118
+ try {
119
+ // Parse the indexes string to ensure it's valid JSON
120
+ const parsedIndexes = JSON.parse(indexes);
121
+ // Ensure indexes is an array of arrays
122
+ if (!Array.isArray(parsedIndexes) ||
123
+ !parsedIndexes.every((pair) => Array.isArray(pair) && pair.length === 2)) {
124
+ return {
125
+ isError: true,
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: 'Indexes must be a JSON array of pairs, e.g. [["movies", "movies_new"]]',
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ const response = await apiClient.post("/swap-indexes", parsedIndexes);
135
+ return {
136
+ content: [
137
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
138
+ ],
139
+ };
140
+ }
141
+ catch (error) {
142
+ return createErrorResponse(error);
143
+ }
144
+ });
145
+ };
146
+ export default registerIndexTools;
@@ -0,0 +1,221 @@
1
+ import { z } from "zod";
2
+ import apiClient from "../utils/api-handler.js";
3
+ import { createErrorResponse } from "../utils/error-handler.js";
4
+ /**
5
+ * Register search tools with the MCP server
6
+ *
7
+ * @param server - The MCP server instance
8
+ */
9
+ export const registerSearchTools = (server) => {
10
+ // Search in an index
11
+ server.tool("search", "Search for documents in a Meilisearch index", {
12
+ indexUid: z.string().describe("Unique identifier of the index"),
13
+ q: z.string().describe("Search query"),
14
+ limit: z
15
+ .number()
16
+ .min(1)
17
+ .optional()
18
+ .describe("Maximum number of results to return (default: 20)"),
19
+ offset: z
20
+ .number()
21
+ .min(0)
22
+ .optional()
23
+ .describe("Number of results to skip (default: 0)"),
24
+ filter: z.string().optional().describe("Filter query to apply"),
25
+ sort: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe('Attributes to sort by, e.g. ["price:asc"]'),
29
+ facets: z.array(z.string()).optional().describe("Facets to return"),
30
+ attributesToRetrieve: z
31
+ .array(z.string())
32
+ .optional()
33
+ .describe("Attributes to include in results"),
34
+ attributesToCrop: z
35
+ .array(z.string())
36
+ .optional()
37
+ .describe("Attributes to crop"),
38
+ cropLength: z
39
+ .number()
40
+ .optional()
41
+ .describe("Length at which to crop cropped attributes"),
42
+ attributesToHighlight: z
43
+ .array(z.string())
44
+ .optional()
45
+ .describe("Attributes to highlight"),
46
+ highlightPreTag: z
47
+ .string()
48
+ .optional()
49
+ .describe("Tag to insert before highlighted text"),
50
+ highlightPostTag: z
51
+ .string()
52
+ .optional()
53
+ .describe("Tag to insert after highlighted text"),
54
+ showMatchesPosition: z
55
+ .boolean()
56
+ .optional()
57
+ .describe("Whether to include match positions in results"),
58
+ matchingStrategy: z
59
+ .string()
60
+ .optional()
61
+ .describe("Matching strategy: 'all' or 'last'"),
62
+ }, async ({ indexUid, q, limit, offset, filter, sort, facets, attributesToRetrieve, attributesToCrop, cropLength, attributesToHighlight, highlightPreTag, highlightPostTag, showMatchesPosition, matchingStrategy, }) => {
63
+ try {
64
+ const response = await apiClient.post(`/indexes/${indexUid}/search`, {
65
+ q,
66
+ limit,
67
+ offset,
68
+ filter,
69
+ sort,
70
+ facets,
71
+ attributesToRetrieve,
72
+ attributesToCrop,
73
+ cropLength,
74
+ attributesToHighlight,
75
+ highlightPreTag,
76
+ highlightPostTag,
77
+ showMatchesPosition,
78
+ matchingStrategy,
79
+ });
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
+ // Multi-search across multiple indexes
91
+ server.tool("multi-search", "Perform multiple searches in one request", {
92
+ searches: z
93
+ .string()
94
+ .describe("JSON array of search queries, each with indexUid and q fields"),
95
+ }, async ({ searches }) => {
96
+ try {
97
+ // Parse the searches string to ensure it's valid JSON
98
+ const parsedSearches = JSON.parse(searches);
99
+ // Ensure searches is an array
100
+ if (!Array.isArray(parsedSearches)) {
101
+ return {
102
+ isError: true,
103
+ content: [{ type: "text", text: "Searches must be a JSON array" }],
104
+ };
105
+ }
106
+ // Ensure each search has at least indexUid
107
+ for (const search of parsedSearches) {
108
+ if (!search.indexUid) {
109
+ return {
110
+ isError: true,
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: "Each search must have an indexUid field",
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ }
120
+ const response = await apiClient.post("/multi-search", {
121
+ queries: parsedSearches,
122
+ });
123
+ return {
124
+ content: [
125
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
126
+ ],
127
+ };
128
+ }
129
+ catch (error) {
130
+ return createErrorResponse(error);
131
+ }
132
+ });
133
+ server.tool("search-across-all-indexes", "Search for a term across all available Meilisearch indexes and return combined results.", {
134
+ q: z.string().describe("Search query"),
135
+ limit: z
136
+ .number()
137
+ .min(1)
138
+ .optional()
139
+ .describe("Maximum number of results to return per index (default: 20)"),
140
+ attributesToRetrieve: z
141
+ .array(z.string())
142
+ .optional()
143
+ .describe("Attributes to include in results"),
144
+ }, async ({ q, limit, attributesToRetrieve }) => {
145
+ try {
146
+ const indexesResponse = await apiClient.get("/indexes", {
147
+ params: { limit: 1000 },
148
+ });
149
+ const indexUids = indexesResponse.data.results.map((index) => index.uid);
150
+ if (!indexUids || indexUids.length === 0) {
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: JSON.stringify({ allHits: [], message: "No indexes found in Meilisearch." }, null, 2),
156
+ },
157
+ ],
158
+ };
159
+ }
160
+ const searchPromises = indexUids.map(async (uid) => {
161
+ try {
162
+ const searchResult = await apiClient.post(`/indexes/${uid}/search`, {
163
+ q,
164
+ limit,
165
+ attributesToRetrieve,
166
+ });
167
+ return searchResult.data.hits.map((hit) => ({
168
+ indexUid: uid,
169
+ ...hit,
170
+ }));
171
+ }
172
+ catch (searchError) {
173
+ return [];
174
+ }
175
+ });
176
+ const resultsPerIndex = await Promise.all(searchPromises);
177
+ const allHits = resultsPerIndex.flat();
178
+ return {
179
+ content: [
180
+ { type: "text", text: JSON.stringify({ allHits }, null, 2) },
181
+ ],
182
+ };
183
+ }
184
+ catch (error) {
185
+ return createErrorResponse(error);
186
+ }
187
+ });
188
+ // Facet search
189
+ server.tool("facet-search", "Search for facet values matching specific criteria", {
190
+ indexUid: z.string().describe("Unique identifier of the index"),
191
+ facetName: z.string().describe("Name of the facet to search"),
192
+ facetQuery: z
193
+ .string()
194
+ .optional()
195
+ .describe("Query to match against facet values"),
196
+ filter: z
197
+ .string()
198
+ .optional()
199
+ .describe("Filter to apply to the base search"),
200
+ }, async ({ indexUid, facetName, facetQuery, filter }) => {
201
+ try {
202
+ const params = {
203
+ facetName,
204
+ };
205
+ if (facetQuery !== undefined)
206
+ params.facetQuery = facetQuery;
207
+ if (filter)
208
+ params.filter = filter;
209
+ const response = await apiClient.post(`/indexes/${indexUid}/facet-search`, params);
210
+ return {
211
+ content: [
212
+ { type: "text", text: JSON.stringify(response.data, null, 2) },
213
+ ],
214
+ };
215
+ }
216
+ catch (error) {
217
+ return createErrorResponse(error);
218
+ }
219
+ });
220
+ };
221
+ export default registerSearchTools;