meilisearch/crates/meilisearch-mcp/src/conversion_tests.rs
Thomas Payet 3b18cddf57 fix: implement proper MCP SSE transport and JSON-RPC compliance
- Fixed SSE handler to send proper 'endpoint' event as per MCP spec
- Added CORS headers for browser-based MCP clients
- Fixed camelCase serialization for JSON-RPC compatibility
- Added session management support with Mcp-Session-Id header
- Improved connection handling with proper keepalive messages
- Added OPTIONS handler for CORS preflight requests

The MCP server now properly implements the SSE transport specification
and is compatible with standard MCP clients like mcpinspector.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-27 14:34:00 +02:00

358 lines
11 KiB
Rust

use crate::registry::{McpTool, McpToolRegistry};
use serde_json::json;
use utoipa::openapi::{OpenApi, PathItem};
#[test]
fn test_convert_simple_get_endpoint() {
let tool = McpTool::from_openapi_path(
"/indexes/{index_uid}",
"GET",
&create_mock_path_item_get(),
);
assert_eq!(tool.name, "getIndex");
assert_eq!(tool.description, "Get information about an index");
assert_eq!(tool.http_method, "GET");
assert_eq!(tool.path_template, "/indexes/{index_uid}");
let schema = &tool.input_schema;
assert_eq!(schema["type"], "object");
assert_eq!(schema["required"], json!(["indexUid"]));
assert_eq!(schema["properties"]["indexUid"]["type"], "string");
}
#[test]
fn test_convert_search_endpoint_with_query_params() {
let tool = McpTool::from_openapi_path(
"/indexes/{index_uid}/search",
"POST",
&create_mock_search_path_item(),
);
assert_eq!(tool.name, "searchDocuments");
assert_eq!(tool.description, "Search for documents in an index");
assert_eq!(tool.http_method, "POST");
let schema = &tool.input_schema;
assert_eq!(schema["type"], "object");
assert_eq!(schema["required"], json!(["indexUid"]));
assert!(schema["properties"]["q"].is_object());
assert!(schema["properties"]["limit"].is_object());
assert!(schema["properties"]["offset"].is_object());
assert!(schema["properties"]["filter"].is_object());
}
#[test]
fn test_convert_document_addition_endpoint() {
let tool = McpTool::from_openapi_path(
"/indexes/{index_uid}/documents",
"POST",
&create_mock_add_documents_path_item(),
);
assert_eq!(tool.name, "addDocuments");
assert_eq!(tool.description, "Add or replace documents in an index");
assert_eq!(tool.http_method, "POST");
let schema = &tool.input_schema;
assert_eq!(schema["type"], "object");
assert_eq!(schema["required"], json!(["indexUid", "documents"]));
assert_eq!(schema["properties"]["documents"]["type"], "array");
}
#[test]
fn test_registry_deduplication() {
let mut registry = McpToolRegistry::new();
let tool1 = McpTool {
name: "searchDocuments".to_string(),
description: "Search documents".to_string(),
input_schema: json!({}),
http_method: "POST".to_string(),
path_template: "/indexes/{index_uid}/search".to_string(),
};
let tool2 = McpTool {
name: "searchDocuments".to_string(),
description: "Updated description".to_string(),
input_schema: json!({"updated": true}),
http_method: "POST".to_string(),
path_template: "/indexes/{index_uid}/search".to_string(),
};
registry.register_tool(tool1);
registry.register_tool(tool2);
assert_eq!(registry.list_tools().len(), 1);
assert_eq!(registry.get_tool("searchDocuments").unwrap().description, "Updated description");
}
#[test]
fn test_openapi_to_mcp_tool_conversion() {
let openapi = create_mock_openapi();
let registry = McpToolRegistry::from_openapi(&openapi);
let tools = registry.list_tools();
assert!(tools.len() > 0);
let search_tool = registry.get_tool("searchDocuments");
assert!(search_tool.is_some());
let index_tool = registry.get_tool("getIndex");
assert!(index_tool.is_some());
}
#[test]
fn test_tool_name_generation() {
let test_cases = vec![
("/indexes", "GET", "getIndexes"),
("/indexes", "POST", "createIndex"),
("/indexes/{index_uid}", "GET", "getIndex"),
("/indexes/{index_uid}", "PUT", "updateIndex"),
("/indexes/{index_uid}", "DELETE", "deleteIndex"),
("/indexes/{index_uid}/documents", "GET", "getDocuments"),
("/indexes/{index_uid}/documents", "POST", "addDocuments"),
("/indexes/{index_uid}/documents", "DELETE", "deleteDocuments"),
("/indexes/{index_uid}/search", "POST", "searchDocuments"),
("/indexes/{index_uid}/settings", "GET", "getSettings"),
("/indexes/{index_uid}/settings", "PATCH", "updateSettings"),
("/tasks", "GET", "getTasks"),
("/tasks/{task_uid}", "GET", "getTask"),
("/keys", "GET", "getApiKeys"),
("/keys", "POST", "createApiKey"),
("/multi-search", "POST", "multiSearch"),
("/swap-indexes", "POST", "swapIndexes"),
];
for (path, method, expected_name) in test_cases {
let name = McpTool::generate_tool_name(path, method);
assert_eq!(name, expected_name, "Path: {}, Method: {}", path, method);
}
}
#[test]
fn test_parameter_extraction() {
let tool = McpTool::from_openapi_path(
"/indexes/{index_uid}/documents/{document_id}",
"GET",
&create_mock_get_document_path_item(),
);
let schema = &tool.input_schema;
assert_eq!(schema["required"], json!(["indexUid", "documentId"]));
assert_eq!(schema["properties"]["indexUid"]["type"], "string");
assert_eq!(schema["properties"]["documentId"]["type"], "string");
}
fn create_mock_path_item_get() -> PathItem {
serde_json::from_value(json!({
"get": {
"summary": "Get information about an index",
"parameters": [
{
"name": "index_uid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Index information"
}
}
}
}))
.unwrap()
}
fn create_mock_search_path_item() -> PathItem {
serde_json::from_value(json!({
"post": {
"summary": "Search for documents in an index",
"parameters": [
{
"name": "index_uid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": "integer",
"default": 20
},
"offset": {
"type": "integer",
"default": 0
},
"filter": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Search results"
}
}
}
}))
.unwrap()
}
fn create_mock_add_documents_path_item() -> PathItem {
serde_json::from_value(json!({
"post": {
"summary": "Add or replace documents in an index",
"parameters": [
{
"name": "index_uid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object"
}
}
}
}
},
"responses": {
"202": {
"description": "Accepted"
}
}
}
}))
.unwrap()
}
fn create_mock_get_document_path_item() -> PathItem {
serde_json::from_value(json!({
"get": {
"summary": "Get a specific document",
"parameters": [
{
"name": "index_uid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "document_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Document found"
}
}
}
}))
.unwrap()
}
fn create_mock_openapi() -> OpenApi {
serde_json::from_value(json!({
"openapi": "3.1.0",
"info": {
"title": "Meilisearch API",
"version": "1.0.0"
},
"paths": {
"/indexes": {
"get": {
"summary": "List all indexes",
"responses": {
"200": {
"description": "List of indexes"
}
}
},
"post": {
"summary": "Create an index",
"responses": {
"202": {
"description": "Index created"
}
}
}
},
"/indexes/{index_uid}": {
"get": {
"summary": "Get information about an index",
"parameters": [
{
"name": "index_uid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Index information"
}
}
}
},
"/indexes/{index_uid}/search": {
"post": {
"summary": "Search for documents in an index",
"parameters": [
{
"name": "index_uid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Search results"
}
}
}
}
}
}))
.unwrap()
}