From dec0b7702ac9ce2dd0744ae082d9198a8987ff71 Mon Sep 17 00:00:00 2001 From: Daniel Bauer Date: Sat, 5 Jul 2025 12:32:43 +0200 Subject: [PATCH] feat: add pagination support to search_objects MCP tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/cordra_mcp/server.py | 13 ++++++++--- tests/test_server.py | 50 ++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/cordra_mcp/server.py b/src/cordra_mcp/server.py index 1d7b8fe..4ce0f21 100644 --- a/src/cordra_mcp/server.py +++ b/src/cordra_mcp/server.py @@ -35,14 +35,20 @@ Examples: - /author:smith - Find objects by author Smith - /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.""" ) async def search_objects( query: str, type: str | None = None, limit: int | None = None, + page_num: int = 0, ) -> str: - """Search for digital objects in the Cordra repository. + """Search for digital objects in the Cordra repository with pagination support. Args: query: The search query string (Lucene/Solr compatible). Examples: @@ -50,7 +56,8 @@ async def search_objects( - "/author:smith" - Find objects by author Smith - "/name:John AND type:Person" - Complex queries 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: JSON string containing list of matching objects with their full metadata @@ -58,7 +65,7 @@ async def search_objects( try: # Use provided limit or default page size of 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"] return json.dumps(results, indent=2) diff --git a/tests/test_server.py b/tests/test_server.py index df7d83c..daf66ae 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -311,7 +311,7 @@ class TestSearchObjects: assert parsed_result[1]["id"] == "people/jane-smith" # 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') async def test_search_objects_with_type_filter(self, mock_client): @@ -334,7 +334,7 @@ class TestSearchObjects: assert parsed_result[0]["type"] == "Person" # 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') async def test_search_objects_with_limit(self, mock_client): @@ -356,7 +356,7 @@ class TestSearchObjects: assert len(parsed_result) == 1 # 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') async def test_search_objects_with_all_parameters(self, mock_client): @@ -379,7 +379,7 @@ class TestSearchObjects: assert parsed_result[0]["type"] == "Document" # 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') async def test_search_objects_empty_results(self, mock_client): @@ -398,7 +398,7 @@ class TestSearchObjects: parsed_result = json.loads(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') async def test_search_objects_client_error(self, mock_client): @@ -409,7 +409,7 @@ class TestSearchObjects: await search_objects("test:query") 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') async def test_search_objects_value_error(self, mock_client): @@ -420,7 +420,7 @@ class TestSearchObjects: await search_objects("invalid:query") 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') 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]["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: """Test the get_cordra_design resource handler."""