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.
- package/README.md +108 -0
- package/dist/client.js +146 -0
- package/dist/config.js +20 -0
- package/dist/http.js +2 -0
- package/dist/index.js +2 -0
- package/dist/server.js +383 -0
- package/dist/stdio.js +2 -0
- package/dist/tools/document-tools.js +208 -0
- package/dist/tools/index-tools.js +146 -0
- package/dist/tools/search-tools.js +221 -0
- package/dist/tools/settings-tools.js +309 -0
- package/dist/tools/system-tools.js +210 -0
- package/dist/tools/task-tools.js +190 -0
- package/dist/tools/vector-tools.js +214 -0
- package/dist/utils/api-handler.js +33 -0
- package/dist/utils/error-handler.js +35 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# MCP Meilisearch API Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server implementation that provides a bridge between AI models and the Meilisearch search engine using the StreamableHTTP transport. This project enables seamless integration of Meilisearch's powerful search capabilities within AI workflows.
|
|
4
|
+
|
|
5
|
+
## Updated Overview
|
|
6
|
+
|
|
7
|
+
This project provides a MCP server that enables AI models to interact directly with Meilisearch functionalities. The architecture includes:
|
|
8
|
+
|
|
9
|
+
- **MCP Server**: Exposes Meilisearch APIs as tools using the Model Context Protocol.
|
|
10
|
+
- **Web Client**: A demo interface showcasing search functionalities.
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
|
16
|
+
│ Meilisearch │ │ MCP Server │ │ Web Client │
|
|
17
|
+
│ Instance │ <--> │ (Node.js) │ <--> │ (Browser) │
|
|
18
|
+
└───────────────┘ └───────────────┘ └───────────────┘
|
|
19
|
+
^
|
|
20
|
+
│
|
|
21
|
+
┌───────────────┐
|
|
22
|
+
│ Document Data │
|
|
23
|
+
│ Sources │
|
|
24
|
+
└───────────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Key Features
|
|
28
|
+
|
|
29
|
+
- **StreamableHTTP Transport**: Real-time communication between clients and the server.
|
|
30
|
+
- **Meilisearch API Support**: Full access to Meilisearch functionalities.
|
|
31
|
+
- **Enhanced Error Handling**: Improved error management for API requests.
|
|
32
|
+
- **Web Client Demo**: Updated interface for demonstrating search capabilities.
|
|
33
|
+
|
|
34
|
+
## Tool Categories
|
|
35
|
+
|
|
36
|
+
The MCP server organizes Meilisearch APIs into these categories:
|
|
37
|
+
|
|
38
|
+
1. **System Tools**: Health checks, version info, server stats.
|
|
39
|
+
2. **Index Tools**: Manage indexes (create, update, delete, list).
|
|
40
|
+
3. **Document Tools**: Add, update, delete, and retrieve documents.
|
|
41
|
+
4. **Search Tools**: Advanced search, including vector search.
|
|
42
|
+
5. **Settings Tools**: Configure index settings.
|
|
43
|
+
6. **Task Tools**: Manage asynchronous tasks.
|
|
44
|
+
7. **Vector Tools**: Experimental vector search capabilities.
|
|
45
|
+
|
|
46
|
+
## Getting Started
|
|
47
|
+
|
|
48
|
+
### Prerequisites
|
|
49
|
+
|
|
50
|
+
- Node.js v20 or higher.
|
|
51
|
+
- A running Meilisearch instance (local or remote).
|
|
52
|
+
- API key for Meilisearch (if required).
|
|
53
|
+
|
|
54
|
+
### Setup Instructions
|
|
55
|
+
|
|
56
|
+
1. Clone the repository:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone <repository-url>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. Install dependencies:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
3. Configure the environment:
|
|
69
|
+
|
|
70
|
+
Create a `.env` file with the following content:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
MEILISEARCH_HOST=http://localhost:7700
|
|
74
|
+
MEILISEARCH_API_KEY=your_master_key_here
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Running the Server
|
|
78
|
+
|
|
79
|
+
To start the server:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run start:server # Start the MCP server
|
|
83
|
+
npm run start:client # Start the web client
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Accessing the Web Interface
|
|
87
|
+
|
|
88
|
+
Visit the following URL in your browser:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
http://localhost:8000
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
This project uses:
|
|
97
|
+
|
|
98
|
+
- **TypeScript**: Ensures type safety.
|
|
99
|
+
- **Express**: Powers the web server.
|
|
100
|
+
- **Model Context Protocol SDK**: Facilitates AI integration.
|
|
101
|
+
|
|
102
|
+
## Project Structure
|
|
103
|
+
|
|
104
|
+
- `src/`: Core MCP server implementation.
|
|
105
|
+
- `tools/`: Meilisearch API tools.
|
|
106
|
+
- `utils/`: Utility functions for API communication and error handling.
|
|
107
|
+
- `server.ts`: Main MCP server implementation.
|
|
108
|
+
- `client/`: Web client for testing and demonstration.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { TextContentSchema, LoggingMessageNotificationSchema, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
export class MCPClient {
|
|
5
|
+
tools = [];
|
|
6
|
+
client;
|
|
7
|
+
tries = 0;
|
|
8
|
+
transport = null;
|
|
9
|
+
onToolsUpdatedCallback = null;
|
|
10
|
+
constructor(serverName) {
|
|
11
|
+
this.client = new Client({
|
|
12
|
+
name: serverName,
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
setOnToolsUpdatedCallback(callback) {
|
|
17
|
+
this.onToolsUpdatedCallback = callback;
|
|
18
|
+
}
|
|
19
|
+
async connectToServer(serverUrl) {
|
|
20
|
+
const url = new URL(serverUrl);
|
|
21
|
+
try {
|
|
22
|
+
console.log(`Connecting to MCP server at ${serverUrl}...`);
|
|
23
|
+
this.transport = new StreamableHTTPClientTransport(url);
|
|
24
|
+
await this.client.connect(this.transport);
|
|
25
|
+
console.log("✅ Successfully connected to server");
|
|
26
|
+
this.setUpTransport();
|
|
27
|
+
this.setUpNotifications();
|
|
28
|
+
await this.listTools();
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
this.tries++;
|
|
32
|
+
if (this.tries > 5) {
|
|
33
|
+
console.error("❌ Failed to connect to MCP server: ", e);
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
console.info(`⚠️ Retry attempt ${this.tries} to connect to server`);
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, this.tries * 1000));
|
|
38
|
+
await this.connectToServer(serverUrl);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async listTools() {
|
|
42
|
+
try {
|
|
43
|
+
console.log("📋 Fetching available tools...");
|
|
44
|
+
const toolsResult = await this.client.listTools();
|
|
45
|
+
if (!toolsResult) {
|
|
46
|
+
console.error("❌ Received null or undefined tools result");
|
|
47
|
+
this.tools = [];
|
|
48
|
+
}
|
|
49
|
+
else if (toolsResult.tools && Array.isArray(toolsResult.tools)) {
|
|
50
|
+
this.tools = toolsResult.tools.map((tool) => ({
|
|
51
|
+
name: tool.name,
|
|
52
|
+
description: tool.description ?? "",
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error("❌ Invalid tools response format:", toolsResult);
|
|
57
|
+
this.tools = [];
|
|
58
|
+
}
|
|
59
|
+
if (this.onToolsUpdatedCallback) {
|
|
60
|
+
this.onToolsUpdatedCallback([...this.tools]);
|
|
61
|
+
}
|
|
62
|
+
if (this.tools.length) {
|
|
63
|
+
console.log(`✅ Successfully loaded ${this.tools.length} tools`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.warn("⚠️ No tools were returned from the server or an error occurred.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error(`❌ Error fetching tools: ${error}`);
|
|
71
|
+
this.tools = [];
|
|
72
|
+
if (this.onToolsUpdatedCallback) {
|
|
73
|
+
this.onToolsUpdatedCallback([...this.tools]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
setUpNotifications() {
|
|
78
|
+
console.log("Setting up notification handlers...");
|
|
79
|
+
this.client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
|
80
|
+
console.log("📢 LoggingMessage received:", notification);
|
|
81
|
+
});
|
|
82
|
+
this.client.setNotificationHandler(ToolListChangedNotificationSchema, async (notification) => {
|
|
83
|
+
console.log("🔄 ToolListChanged notification received:", notification);
|
|
84
|
+
await this.listTools();
|
|
85
|
+
});
|
|
86
|
+
console.log("✅ Notification handlers set up");
|
|
87
|
+
}
|
|
88
|
+
async callTool(name, args) {
|
|
89
|
+
try {
|
|
90
|
+
console.log(`\n🔧 Calling tool: ${name}`);
|
|
91
|
+
console.log("With arguments:", JSON.stringify(args, null, 2));
|
|
92
|
+
const result = await this.client.callTool({ name, arguments: args });
|
|
93
|
+
if (!result) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: "Received null or undefined result from tool",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const content = result.content;
|
|
100
|
+
if (!content?.length) {
|
|
101
|
+
return { success: true, data: [] };
|
|
102
|
+
}
|
|
103
|
+
const processedContent = content.reduce((acc, item) => {
|
|
104
|
+
const parse = TextContentSchema.safeParse(item);
|
|
105
|
+
if (parse.success) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(parse.data.text);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return parse.data.text;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return item;
|
|
114
|
+
}, {});
|
|
115
|
+
return { success: true, data: processedContent };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
119
|
+
return { success: false, error: errorMessage };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
setUpTransport() {
|
|
123
|
+
if (this.transport === null)
|
|
124
|
+
return;
|
|
125
|
+
this.transport.onclose = () => {
|
|
126
|
+
console.log("🔌 Transport closed");
|
|
127
|
+
};
|
|
128
|
+
this.transport.onerror = async (error) => {
|
|
129
|
+
console.log("❌ Transport error:", error);
|
|
130
|
+
await this.cleanup();
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async cleanup() {
|
|
134
|
+
console.log("Cleaning up resources...");
|
|
135
|
+
try {
|
|
136
|
+
await this.client.close();
|
|
137
|
+
console.log("✅ Client closed successfully");
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error("❌ Error during cleanup:", error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async close() {
|
|
144
|
+
await this.cleanup();
|
|
145
|
+
}
|
|
146
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meilisearch MCP Server Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file contains the configuration settings for connecting to the Meilisearch server.
|
|
5
|
+
* Configuration is loaded from environment variables with sensible defaults.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Load and initialize configuration from environment variables
|
|
9
|
+
*/
|
|
10
|
+
export const loadConfig = () => {
|
|
11
|
+
return {
|
|
12
|
+
host: process.env.MEILISEARCH_HOST || "http://localhost:7700",
|
|
13
|
+
apiKey: process.env.MEILISEARCH_API_KEY || "",
|
|
14
|
+
timeout: parseInt(process.env.MEILISEARCH_TIMEOUT || "5000", 10),
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
// Export the config instance
|
|
18
|
+
export const config = loadConfig();
|
|
19
|
+
// Re-export for direct use
|
|
20
|
+
export default config;
|
package/dist/http.js
ADDED
package/dist/index.js
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import registerTaskTools from "./tools/task-tools.js";
|
|
4
|
+
import registerIndexTools from "./tools/index-tools.js";
|
|
5
|
+
import registerSearchTools from "./tools/search-tools.js";
|
|
6
|
+
import registerSystemTools from "./tools/system-tools.js";
|
|
7
|
+
import registerVectorTools from "./tools/vector-tools.js";
|
|
8
|
+
import registerDocumentTools from "./tools/document-tools.js";
|
|
9
|
+
import registerSettingsTools from "./tools/settings-tools.js";
|
|
10
|
+
import { createErrorResponse } from "./utils/error-handler.js";
|
|
11
|
+
import express from "express";
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
httpPort: 8080,
|
|
17
|
+
mcpEndpoint: "/mcp",
|
|
18
|
+
serverName: "meilisearch",
|
|
19
|
+
serverVersion: "1.0.0",
|
|
20
|
+
sessionCleanupInterval: 60000, // 1 minute
|
|
21
|
+
sessionTimeout: 3600000, // 1 hour
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Implementation of an MCP server for Meilisearch
|
|
25
|
+
*/
|
|
26
|
+
class MCPServer {
|
|
27
|
+
JSON_RPC = "2.0";
|
|
28
|
+
SESSION_ID_HEADER_NAME = "mcp-session-id";
|
|
29
|
+
server;
|
|
30
|
+
sessions = new Map();
|
|
31
|
+
toolsRegistered = false;
|
|
32
|
+
cleanupInterval = null;
|
|
33
|
+
config;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new MCP server instance
|
|
36
|
+
* @param server The underlying MCP server implementation
|
|
37
|
+
* @param config Configuration options
|
|
38
|
+
*/
|
|
39
|
+
constructor(server, config = {}) {
|
|
40
|
+
this.server = server;
|
|
41
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
42
|
+
this.toolsRegistered = this.registerTools();
|
|
43
|
+
// Start session cleanup if using HTTP transport
|
|
44
|
+
this.startSessionCleanup();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Handles an HTTP GET request
|
|
48
|
+
* @param req The HTTP request
|
|
49
|
+
* @param res The HTTP response
|
|
50
|
+
*/
|
|
51
|
+
async handleGetRequest(req, res) {
|
|
52
|
+
console.log("GET request received");
|
|
53
|
+
const sessionId = this.extractSessionId(req);
|
|
54
|
+
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
55
|
+
console.error(`Invalid session ID: ${sessionId}`);
|
|
56
|
+
this.sendErrorResponse(res, 400, "Bad Request: invalid session ID");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(`Establishing HTTP stream for session ${sessionId}`);
|
|
60
|
+
const sessionInfo = this.sessions.get(sessionId);
|
|
61
|
+
const transport = sessionInfo.transport;
|
|
62
|
+
try {
|
|
63
|
+
await transport.handleRequest(req, res);
|
|
64
|
+
this.updateSessionActivity(sessionId);
|
|
65
|
+
this.sendNotification(transport, {
|
|
66
|
+
method: "notifications/message",
|
|
67
|
+
params: { level: "info", data: "HTTP Connection established" },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(`Error handling GET request for session ${sessionId}:`, error);
|
|
72
|
+
this.sendErrorResponse(res, 500, `Internal server error: ${error}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Handles an HTTP POST request (executes MCP commands)
|
|
77
|
+
* @param req The HTTP request
|
|
78
|
+
* @param res The HTTP response
|
|
79
|
+
*/
|
|
80
|
+
async handlePostRequest(req, res) {
|
|
81
|
+
const sessionId = this.extractSessionId(req);
|
|
82
|
+
try {
|
|
83
|
+
// Case 1: Existing session
|
|
84
|
+
if (sessionId && this.sessions.has(sessionId)) {
|
|
85
|
+
console.log(`POST request for existing session ${sessionId}`);
|
|
86
|
+
const sessionInfo = this.sessions.get(sessionId);
|
|
87
|
+
await sessionInfo.transport.handleRequest(req, res, req.body);
|
|
88
|
+
this.updateSessionActivity(sessionId);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Case 2: Initialize request
|
|
92
|
+
if (!sessionId && this.isInitializeRequest(req.body)) {
|
|
93
|
+
await this.handleInitializeRequest(req, res);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Case 3: Invalid request
|
|
97
|
+
console.error("Invalid request: missing session ID or not an initialize request");
|
|
98
|
+
this.sendErrorResponse(res, 400, "Bad Request: invalid session ID or not an initialize request");
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error("Error handling MCP request:", error);
|
|
102
|
+
this.sendErrorResponse(res, 500, `Internal server error: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Clean up and release server resources
|
|
107
|
+
*/
|
|
108
|
+
shutdown() {
|
|
109
|
+
console.log("Shutting down MCP server...");
|
|
110
|
+
if (this.cleanupInterval) {
|
|
111
|
+
clearInterval(this.cleanupInterval);
|
|
112
|
+
this.cleanupInterval = null;
|
|
113
|
+
}
|
|
114
|
+
// Close all active sessions
|
|
115
|
+
for (const [sessionId, sessionInfo] of this.sessions.entries()) {
|
|
116
|
+
try {
|
|
117
|
+
console.log(`Closing session ${sessionId}`);
|
|
118
|
+
sessionInfo.transport.close();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error(`Error closing session ${sessionId}:`, error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.sessions.clear();
|
|
125
|
+
console.log("MCP server shutdown complete");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Handles the initial connection request
|
|
129
|
+
* @param req The HTTP request
|
|
130
|
+
* @param res The HTTP response
|
|
131
|
+
*/
|
|
132
|
+
async handleInitializeRequest(req, res) {
|
|
133
|
+
console.log("Handling initialize request");
|
|
134
|
+
const newSessionId = randomUUID();
|
|
135
|
+
const transport = new StreamableHTTPServerTransport({
|
|
136
|
+
sessionIdGenerator: () => newSessionId,
|
|
137
|
+
});
|
|
138
|
+
transport.sessionId = newSessionId;
|
|
139
|
+
try {
|
|
140
|
+
// Connect the transport to the MCP server
|
|
141
|
+
await this.server.connect(transport);
|
|
142
|
+
// Set response headers
|
|
143
|
+
res.setHeader(this.SESSION_ID_HEADER_NAME, newSessionId);
|
|
144
|
+
res.setHeader("Access-Control-Expose-Headers", this.SESSION_ID_HEADER_NAME);
|
|
145
|
+
// Handle the initialize request
|
|
146
|
+
await transport.handleRequest(req, res, req.body);
|
|
147
|
+
// Register the session
|
|
148
|
+
this.sessions.set(newSessionId, {
|
|
149
|
+
transport,
|
|
150
|
+
lastActivity: Date.now(),
|
|
151
|
+
});
|
|
152
|
+
// Notify client about available tools
|
|
153
|
+
this.sendToolListChangedNotification(transport);
|
|
154
|
+
console.log(`New session established: ${newSessionId}`);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("Error handling initialize request:", error);
|
|
158
|
+
transport.close();
|
|
159
|
+
this.sendErrorResponse(res, 500, `Failed to initialize: ${error}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Registers all available tools with the MCP server
|
|
164
|
+
*/
|
|
165
|
+
registerTools() {
|
|
166
|
+
try {
|
|
167
|
+
console.log("Registering MCP tools...");
|
|
168
|
+
const tools = [
|
|
169
|
+
registerSystemTools,
|
|
170
|
+
registerIndexTools,
|
|
171
|
+
registerSearchTools,
|
|
172
|
+
registerSettingsTools,
|
|
173
|
+
registerDocumentTools,
|
|
174
|
+
registerTaskTools,
|
|
175
|
+
registerVectorTools,
|
|
176
|
+
];
|
|
177
|
+
tools.forEach((registerFn) => registerFn(this.server));
|
|
178
|
+
console.log("Successfully registered all tools");
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.error("Failed to register tools:", error);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Sends a notification about tool list changes
|
|
188
|
+
*/
|
|
189
|
+
sendToolListChangedNotification(transport) {
|
|
190
|
+
const notification = {
|
|
191
|
+
method: "notifications/tools/list_changed",
|
|
192
|
+
params: {},
|
|
193
|
+
};
|
|
194
|
+
this.sendNotification(transport, notification);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Sends a notification through the transport
|
|
198
|
+
*/
|
|
199
|
+
async sendNotification(transport, notification) {
|
|
200
|
+
try {
|
|
201
|
+
const rpcNotification = {
|
|
202
|
+
...notification,
|
|
203
|
+
jsonrpc: this.JSON_RPC,
|
|
204
|
+
};
|
|
205
|
+
await transport.send(rpcNotification);
|
|
206
|
+
console.log(`Sent notification: ${notification.method}`);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error(`Failed to send notification ${notification.method}:`, error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Checks if the request body represents an initialize request
|
|
214
|
+
*/
|
|
215
|
+
isInitializeRequest(body) {
|
|
216
|
+
const isInitial = (data) => {
|
|
217
|
+
const result = InitializeRequestSchema.safeParse(data);
|
|
218
|
+
return result.success;
|
|
219
|
+
};
|
|
220
|
+
if (Array.isArray(body)) {
|
|
221
|
+
return body.some(isInitial);
|
|
222
|
+
}
|
|
223
|
+
return isInitial(body);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Extracts session ID from request headers
|
|
227
|
+
*/
|
|
228
|
+
extractSessionId(req) {
|
|
229
|
+
return req.headers[this.SESSION_ID_HEADER_NAME.toLowerCase()];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Updates the activity timestamp for a session
|
|
233
|
+
*/
|
|
234
|
+
updateSessionActivity(sessionId) {
|
|
235
|
+
const sessionInfo = this.sessions.get(sessionId);
|
|
236
|
+
if (sessionInfo) {
|
|
237
|
+
sessionInfo.lastActivity = Date.now();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Sends an error response with the specified status code and message
|
|
242
|
+
*/
|
|
243
|
+
sendErrorResponse(res, status, message) {
|
|
244
|
+
res.status(status).json(createErrorResponse(message));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Starts the session cleanup process
|
|
248
|
+
*/
|
|
249
|
+
startSessionCleanup() {
|
|
250
|
+
this.cleanupInterval = setInterval(() => {
|
|
251
|
+
this.cleanupExpiredSessions();
|
|
252
|
+
}, this.config.sessionCleanupInterval);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Removes expired sessions
|
|
256
|
+
*/
|
|
257
|
+
cleanupExpiredSessions() {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
const expiredIds = [];
|
|
260
|
+
// Find expired sessions
|
|
261
|
+
for (const [sessionId, info] of this.sessions.entries()) {
|
|
262
|
+
if (now - info.lastActivity > this.config.sessionTimeout) {
|
|
263
|
+
expiredIds.push(sessionId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Remove expired sessions
|
|
267
|
+
if (expiredIds.length > 0) {
|
|
268
|
+
console.log(`Cleaning up ${expiredIds.length} expired sessions`);
|
|
269
|
+
for (const sessionId of expiredIds) {
|
|
270
|
+
try {
|
|
271
|
+
const info = this.sessions.get(sessionId);
|
|
272
|
+
if (info) {
|
|
273
|
+
info.transport.close();
|
|
274
|
+
}
|
|
275
|
+
this.sessions.delete(sessionId);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
console.error(`Error closing expired session ${sessionId}:`, error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Initialize the MCP server with HTTP transport
|
|
286
|
+
*/
|
|
287
|
+
const initServerHTTPTransport = () => {
|
|
288
|
+
const config = DEFAULT_CONFIG;
|
|
289
|
+
const serverInstance = new McpServer({
|
|
290
|
+
name: config.serverName,
|
|
291
|
+
version: config.serverVersion,
|
|
292
|
+
});
|
|
293
|
+
const server = new MCPServer(serverInstance, config);
|
|
294
|
+
const app = express();
|
|
295
|
+
app.use(express.json());
|
|
296
|
+
// Configure CORS and preflight handling
|
|
297
|
+
app.use((req, res, next) => {
|
|
298
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
299
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
300
|
+
res.header("Access-Control-Allow-Headers", `Origin, X-Requested-With, Content-Type, Accept, ${server["SESSION_ID_HEADER_NAME"]}`);
|
|
301
|
+
if (req.method === "OPTIONS") {
|
|
302
|
+
res.sendStatus(200);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
next();
|
|
306
|
+
});
|
|
307
|
+
// Set up routes
|
|
308
|
+
const router = express.Router();
|
|
309
|
+
router.post(config.mcpEndpoint, async (req, res) => {
|
|
310
|
+
console.log(`Received POST request to ${config.mcpEndpoint}`);
|
|
311
|
+
await server.handlePostRequest(req, res);
|
|
312
|
+
});
|
|
313
|
+
router.get(config.mcpEndpoint, async (req, res) => {
|
|
314
|
+
console.log(`Received GET request to ${config.mcpEndpoint}`);
|
|
315
|
+
await server.handleGetRequest(req, res);
|
|
316
|
+
});
|
|
317
|
+
app.use("/", router);
|
|
318
|
+
// Start the server
|
|
319
|
+
const httpServer = app.listen(config.httpPort, () => {
|
|
320
|
+
console.log("Meilisearch MCP Server is running on http transport:", `http://localhost:${config.httpPort}${config.mcpEndpoint}`);
|
|
321
|
+
});
|
|
322
|
+
// Handle server shutdown
|
|
323
|
+
process.on("SIGINT", async () => {
|
|
324
|
+
console.log("Received SIGINT signal");
|
|
325
|
+
server.shutdown();
|
|
326
|
+
httpServer.close(() => {
|
|
327
|
+
console.log("HTTP server closed");
|
|
328
|
+
process.exit(0);
|
|
329
|
+
});
|
|
330
|
+
// Force exit after timeout
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
console.log("Forcing process exit");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}, 5000);
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Initialize the MCP server with stdio transport
|
|
339
|
+
*/
|
|
340
|
+
const initServerStdioTransport = async () => {
|
|
341
|
+
const config = DEFAULT_CONFIG;
|
|
342
|
+
const server = new McpServer({
|
|
343
|
+
name: config.serverName,
|
|
344
|
+
version: config.serverVersion,
|
|
345
|
+
});
|
|
346
|
+
// Register all tools
|
|
347
|
+
registerIndexTools(server);
|
|
348
|
+
registerDocumentTools(server);
|
|
349
|
+
registerSearchTools(server);
|
|
350
|
+
registerSettingsTools(server);
|
|
351
|
+
registerVectorTools(server);
|
|
352
|
+
registerSystemTools(server);
|
|
353
|
+
registerTaskTools(server);
|
|
354
|
+
// Connect stdio transport
|
|
355
|
+
const transport = new StdioServerTransport();
|
|
356
|
+
await server.connect(transport);
|
|
357
|
+
console.log("Meilisearch MCP Server is running on stdio transport");
|
|
358
|
+
// Handle process termination
|
|
359
|
+
process.on("SIGINT", () => {
|
|
360
|
+
console.log("Shutting down stdio server...");
|
|
361
|
+
process.exit(0);
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Initialize the MCP server with the specified transport
|
|
366
|
+
* @param transport The transport type to use ("stdio" or "http")
|
|
367
|
+
* @throws Error if the transport type is unsupported
|
|
368
|
+
*/
|
|
369
|
+
export const initServer = (transport) => {
|
|
370
|
+
switch (transport) {
|
|
371
|
+
case "stdio":
|
|
372
|
+
initServerStdioTransport().catch((error) => {
|
|
373
|
+
console.error("Fatal error initializing stdio transport:", error);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
});
|
|
376
|
+
break;
|
|
377
|
+
case "http":
|
|
378
|
+
initServerHTTPTransport();
|
|
379
|
+
break;
|
|
380
|
+
default:
|
|
381
|
+
throw new Error(`Unsupported transport type: ${transport}`);
|
|
382
|
+
}
|
|
383
|
+
};
|
package/dist/stdio.js
ADDED