feat: add pagination support to search_objects MCP tool

Add page_num parameter to the search_objects tool to enable pagination
control for users. This provides full pagination functionality while
maintaining backward compatibility.

Changes:
- Add page_num parameter with default value of 0 (0-based pagination)
- Update tool description to document pagination parameters
- Enhance docstring to explain page_num and clarify limit as page_size
- Add comprehensive tests for pagination scenarios
- All existing functionality remains unchanged

Users can now paginate through search results:
- search_objects("query") - first page, 20 results
- search_objects("query", limit=50) - first page, 50 results
- search_objects("query", page_num=1) - second page, 20 results
- search_objects("query", limit=10, page_num=5) - sixth page, 10 results

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Daniel Bauer
2025-07-05 12:32:43 +02:00
parent 56e1a85d5e
commit dec0b7702a
2 changed files with 53 additions and 10 deletions

View File

@@ -35,14 +35,20 @@ Examples:
- /author:smith - Find objects by author Smith - /author:smith - Find objects by author Smith
- /name:John AND type:Person - Complex queries - /name:John AND type:Person - Complex queries
Pagination:
- Results are paginated with 0-based page numbering
- Use 'limit' to control page size (default: 20)
- Use 'page_num' to specify which page to retrieve (default: 0)
Returns a JSON list of matching objects with their full metadata.""" Returns a JSON list of matching objects with their full metadata."""
) )
async def search_objects( async def search_objects(
query: str, query: str,
type: str | None = None, type: str | None = None,
limit: int | None = None, limit: int | None = None,
page_num: int = 0,
) -> str: ) -> str:
"""Search for digital objects in the Cordra repository. """Search for digital objects in the Cordra repository with pagination support.
Args: Args:
query: The search query string (Lucene/Solr compatible). Examples: query: The search query string (Lucene/Solr compatible). Examples:
@@ -50,7 +56,8 @@ async def search_objects(
- "/author:smith" - Find objects by author Smith - "/author:smith" - Find objects by author Smith
- "/name:John AND type:Person" - Complex queries - "/name:John AND type:Person" - Complex queries
type: Optional filter by object type (e.g., "Person", "Document", "Project") type: Optional filter by object type (e.g., "Person", "Document", "Project")
limit: Optional limit on number of results (default: config max_search_results) limit: Optional page size - number of results per page (default: 20)
page_num: Page number to retrieve, 0-based (default: 0 for first page)
Returns: Returns:
JSON string containing list of matching objects with their full metadata JSON string containing list of matching objects with their full metadata
@@ -58,7 +65,7 @@ async def search_objects(
try: try:
# Use provided limit or default page size of 20 # Use provided limit or default page size of 20
page_size = limit if limit is not None else 20 page_size = limit if limit is not None else 20
search_result = await cordra_client.find(query, object_type=type, page_size=page_size) search_result = await cordra_client.find(query, object_type=type, page_size=page_size, page_num=page_num)
results = search_result["results"] results = search_result["results"]
return json.dumps(results, indent=2) return json.dumps(results, indent=2)

View File

@@ -311,7 +311,7 @@ class TestSearchObjects:
assert parsed_result[1]["id"] == "people/jane-smith" assert parsed_result[1]["id"] == "people/jane-smith"
# Verify the client was called with correct parameters # Verify the client was called with correct parameters
mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=20) mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=20, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_type_filter(self, mock_client): async def test_search_objects_with_type_filter(self, mock_client):
@@ -334,7 +334,7 @@ class TestSearchObjects:
assert parsed_result[0]["type"] == "Person" assert parsed_result[0]["type"] == "Person"
# Verify the client was called with type filter # Verify the client was called with type filter
mock_client.find.assert_called_once_with("name:John", object_type="Person", page_size=20) mock_client.find.assert_called_once_with("name:John", object_type="Person", page_size=20, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_limit(self, mock_client): async def test_search_objects_with_limit(self, mock_client):
@@ -356,7 +356,7 @@ class TestSearchObjects:
assert len(parsed_result) == 1 assert len(parsed_result) == 1
# Verify the client was called with custom limit # Verify the client was called with custom limit
mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=50) mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=50, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_all_parameters(self, mock_client): async def test_search_objects_with_all_parameters(self, mock_client):
@@ -379,7 +379,7 @@ class TestSearchObjects:
assert parsed_result[0]["type"] == "Document" assert parsed_result[0]["type"] == "Document"
# Verify the client was called with all parameters # Verify the client was called with all parameters
mock_client.find.assert_called_once_with("title:Report", object_type="Document", page_size=25) mock_client.find.assert_called_once_with("title:Report", object_type="Document", page_size=25, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_empty_results(self, mock_client): async def test_search_objects_empty_results(self, mock_client):
@@ -398,7 +398,7 @@ class TestSearchObjects:
parsed_result = json.loads(result) parsed_result = json.loads(result)
assert parsed_result == [] assert parsed_result == []
mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, page_size=20) mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, page_size=20, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_client_error(self, mock_client): async def test_search_objects_client_error(self, mock_client):
@@ -409,7 +409,7 @@ class TestSearchObjects:
await search_objects("test:query") await search_objects("test:query")
assert "Search failed:" in str(exc_info.value) assert "Search failed:" in str(exc_info.value)
mock_client.find.assert_called_once_with("test:query", object_type=None, page_size=20) mock_client.find.assert_called_once_with("test:query", object_type=None, page_size=20, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_value_error(self, mock_client): async def test_search_objects_value_error(self, mock_client):
@@ -420,7 +420,7 @@ class TestSearchObjects:
await search_objects("invalid:query") await search_objects("invalid:query")
assert "Invalid search parameters:" in str(exc_info.value) assert "Invalid search parameters:" in str(exc_info.value)
mock_client.find.assert_called_once_with("invalid:query", object_type=None, page_size=20) mock_client.find.assert_called_once_with("invalid:query", object_type=None, page_size=20, page_num=0)
@patch('cordra_mcp.server.cordra_client') @patch('cordra_mcp.server.cordra_client')
async def test_search_objects_json_formatting(self, mock_client): async def test_search_objects_json_formatting(self, mock_client):
@@ -449,6 +449,42 @@ class TestSearchObjects:
assert parsed_result[0]["type"] == "Test" assert parsed_result[0]["type"] == "Test"
assert parsed_result[0]["content"]["data"] == "value" assert parsed_result[0]["content"]["data"] == "value"
@patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_page_num(self, mock_client):
"""Test object search with page number parameter."""
mock_search_result = {
"results": [
{"id": "documents/doc-21", "type": "Document", "content": {"title": "Page 2 Doc"}},
],
"total_size": 50,
"page_num": 1,
"page_size": 20
}
mock_client.find = AsyncMock(return_value=mock_search_result)
await search_objects("type:Document", page_num=1)
# Verify the client was called with correct page number
mock_client.find.assert_called_once_with("type:Document", object_type=None, page_size=20, page_num=1)
@patch('cordra_mcp.server.cordra_client')
async def test_search_objects_with_all_pagination_params(self, mock_client):
"""Test object search with all pagination parameters."""
mock_search_result = {
"results": [
{"id": "reports/report-51", "type": "Report", "content": {"title": "Report 51"}},
],
"total_size": 100,
"page_num": 5,
"page_size": 10
}
mock_client.find = AsyncMock(return_value=mock_search_result)
await search_objects("type:Report", type="Report", limit=10, page_num=5)
# Verify the client was called with all parameters
mock_client.find.assert_called_once_with("type:Report", object_type="Report", page_size=10, page_num=5)
class TestGetCordraDesign: class TestGetCordraDesign:
"""Test the get_cordra_design resource handler.""" """Test the get_cordra_design resource handler."""