Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/meilisearch_mcp/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,43 @@ async def get_documents(
"""Get documents from an index"""
try:
index = self.client.index(index_uid)
return index.get_documents(
{"offset": offset, "limit": limit, "fields": fields}
)
# Build parameters dict, excluding None values to avoid API errors
params = {}
if offset is not None:
params["offset"] = offset
if limit is not None:
params["limit"] = limit
if fields is not None:
params["fields"] = fields

result = index.get_documents(params if params else {})

# Convert meilisearch model objects to JSON-serializable format
if hasattr(result, '__dict__'):
result_dict = result.__dict__.copy()
# Convert individual document objects in results if they exist
if 'results' in result_dict and isinstance(result_dict['results'], list):
serialized_results = []
for doc in result_dict['results']:
if hasattr(doc, '__dict__'):
# Extract the actual document data
doc_dict = doc.__dict__.copy()
# Look for private attributes that might contain the actual data
for key, value in doc_dict.items():
if key.startswith('_') and isinstance(value, dict):
# Use the dict content instead of the wrapper
serialized_results.append(value)
break
else:
# If no private dict found, use the object dict directly
serialized_results.append(doc_dict)
else:
serialized_results.append(doc)
result_dict['results'] = serialized_results
return result_dict
else:
return result

except Exception as e:
raise Exception(f"Failed to get documents: {str(e)}")

Expand Down
14 changes: 11 additions & 3 deletions src/meilisearch_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ def json_serializer(obj: Any) -> str:
"""Custom JSON serializer for objects not serializable by default json code"""
if isinstance(obj, datetime):
return obj.isoformat()
# Handle Meilisearch model objects by using their __dict__ if available
if hasattr(obj, '__dict__'):
return obj.__dict__
return str(obj)


Expand Down Expand Up @@ -318,13 +321,18 @@ async def handle_call_tool(
]

elif name == "get-documents":
# Use default values to fix None parameter issues (related to issue #17)
offset = arguments.get("offset", 0)
limit = arguments.get("limit", 20)
documents = await self.meili_client.documents.get_documents(
arguments["indexUid"],
arguments.get("offset"),
arguments.get("limit"),
offset,
limit,
)
# Convert DocumentsResults object to proper JSON format (fixes issue #16)
formatted_json = json.dumps(documents, indent=2, default=json_serializer)
return [
types.TextContent(type="text", text=f"Documents: {documents}")
types.TextContent(type="text", text=f"Documents:\n{formatted_json}")
]

elif name == "add-documents":
Expand Down
63 changes: 63 additions & 0 deletions tests/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,69 @@ async def test_get_connection_settings_format(self, server):
assert "********" in text or "Not set" in text
else:
assert "Not set" in text


class TestIssue16GetDocumentsJsonSerialization:
"""Test for issue #16 - get-documents should return JSON, not Python object representations"""

@pytest.fixture
async def server(self):
"""Create server instance for issue #16 tests"""
url = os.getenv("MEILI_HTTP_ADDR", "http://localhost:7700")
api_key = os.getenv("MEILI_MASTER_KEY")
server = create_server(url, api_key)
yield server
await server.cleanup()

async def test_get_documents_returns_json_not_python_object(self, server):
"""Test that get-documents returns JSON-formatted text, not Python object string representation (issue #16)"""
import time
test_index = f"test_issue16_{int(time.time() * 1000)}"

# Create index and add a test document
await simulate_mcp_call(server, "create-index", {"uid": test_index})

test_document = {"id": 1, "title": "Test Document", "content": "Test content"}
await simulate_mcp_call(server, "add-documents", {
"indexUid": test_index,
"documents": [test_document]
})

# Wait for indexing
import asyncio
await asyncio.sleep(0.5)

# Get documents with explicit parameters
result = await simulate_mcp_call(server, "get-documents", {
"indexUid": test_index,
"offset": 0,
"limit": 10
})

assert len(result) == 1
assert result[0].type == "text"

response_text = result[0].text

# Issue #16 assertion: Should NOT contain Python object representation
assert "<meilisearch.models.document.DocumentsResults object at" not in response_text
assert "DocumentsResults" not in response_text

# Should contain proper JSON structure
assert "Documents:" in response_text
assert "Test Document" in response_text # Actual document content should be accessible
assert "Test content" in response_text

# Should be valid JSON after the "Documents:" prefix
json_part = response_text.replace("Documents:", "").strip()
import json
try:
parsed_data = json.loads(json_part)
assert isinstance(parsed_data, dict)
assert "results" in parsed_data
assert len(parsed_data["results"]) > 0
except json.JSONDecodeError:
pytest.fail(f"get-documents returned non-JSON data: {response_text}")

async def test_update_connection_settings_persistence(self, server):
"""Test that connection updates persist for MCP client sessions"""
Expand Down