diff --git a/README.md b/README.md index c76b4dc..c1a8d58 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,11 @@ ensuring safe exploration without risk of data modification or corruption. ### Tools -- `search_objects` - Search for digital objects using a query string. +- `search_objects` - Search for digital objects using a query string with pagination support. + - `query` - Lucene/Solr compatible search query + - `type` - Optional filter by object type + - `limit` - Number of results per page (default: 1) + - `page_num` - Page number to retrieve, 0-based (default: 0) ## Configuration @@ -35,7 +39,6 @@ The MCP server can be configured using environment variables with the `CORDRA_` - `CORDRA_PASSWORD` - Password for authentication (optional) - `CORDRA_VERIFY_SSL` - SSL certificate verification (default: `true`) - `CORDRA_TIMEOUT` - Request timeout in seconds (default: `30`) -- `CORDRA_MAX_SEARCH_RESULTS` - Maximum search results (default: `1000`) ## Usage diff --git a/src/cordra_mcp/client.py b/src/cordra_mcp/client.py index 17eee59..3963b1e 100644 --- a/src/cordra_mcp/client.py +++ b/src/cordra_mcp/client.py @@ -129,16 +129,21 @@ class CordraClient: f"Failed to retrieve object {object_id}: {e}" ) from e - async def find(self, query: str, object_type: str | None = None, limit: int | None = None) -> list[dict[str, Any]]: - """Find objects using a Cordra query. + async def find(self, query: str, object_type: str | None = None, page_size: int = 20, page_num: int = 0) -> dict[str, Any]: + """Find objects using a Cordra query with pagination support. Args: query: The query string to search for objects object_type: Optional filter by object type - limit: Optional limit on number of results + page_size: Number of results per page (if None, no limit) + page_num: Page number to retrieve (0-based, default: 0) Returns: - List of objects matching the query as dictionaries + Dict containing: + - results: List of objects matching the query as dictionaries + - total_size: Total number of results available + - page_num: Current page number + - page_size: Number of results per page Raises: ValueError: If query is empty @@ -151,11 +156,11 @@ class CordraClient: final_query = f"type:{object_type} AND ({query})" url = f"{self.config.base_url}/search" - params = {"query": final_query} - - # Add pageSize if limit is specified - if limit is not None: - params["pageSize"] = str(limit) + params = { + "query": final_query, + "pageSize": str(page_size), + "pageNum": str(page_num), + } try: response = self.session.get(url, params=params, timeout=self.config.timeout) @@ -167,11 +172,12 @@ class CordraClient: search_result = response.json() - # Extract the results array from the response - if isinstance(search_result, dict) and "results" in search_result: - return search_result["results"] # type: ignore - else: - return [] + return { + "results": search_result["results"], + "total_size": search_result["size"], + "page_num": search_result["pageNum"], + "page_size": search_result["pageSize"] + } except requests.RequestException as e: raise CordraClientError( @@ -196,7 +202,8 @@ class CordraClient: query = f"type:Schema AND /name:{schema_name}" try: - schemas = await self.find(query) + search_result = await self.find(query) + schemas = search_result["results"] if not schemas: raise CordraNotFoundError(f"Schema '{schema_name}' not found") diff --git a/src/cordra_mcp/config.py b/src/cordra_mcp/config.py index ee65b50..c677894 100644 --- a/src/cordra_mcp/config.py +++ b/src/cordra_mcp/config.py @@ -22,9 +22,6 @@ class CordraConfig(BaseSettings): password: str | None = Field( default=None, description="Password for Cordra authentication" ) - max_search_results: int = Field( - default=1000, description="Maximum number of search results to return" - ) verify_ssl: bool = Field( default=True, description="Whether to verify SSL certificates" ) diff --git a/src/cordra_mcp/server.py b/src/cordra_mcp/server.py index 4502e0a..54a9d9c 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: 1) +- 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, + limit: int = 1, + 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,14 +56,15 @@ 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: Page size - number of results per page (default: 1) + 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 """ try: - effective_limit = limit if limit is not None else config.max_search_results - results = await cordra_client.find(query, object_type=type, limit=effective_limit) + search_result = await cordra_client.find(query, object_type=type, page_size=limit, page_num=page_num) + results = search_result["results"] return json.dumps(results, indent=2) except ValueError as e: @@ -155,10 +162,23 @@ async def create_schema_resource(schema_name: str) -> str: async def register_schema_resources() -> None: """Register individual schema resources dynamically.""" try: - # Get all available schemas - schemas = await cordra_client.find("type:Schema") + # Get all available schemas using pagination + all_schemas = [] + page_num = 0 + page_size = 20 - for schema in schemas: + while True: + search_result = await cordra_client.find("type:Schema", page_size=page_size, page_num=page_num) + schemas = search_result["results"] + all_schemas.extend(schemas) + + # Check if we've retrieved all schemas + if len(schemas) < page_size: + break + + page_num += 1 + + for schema in all_schemas: schema_name = schema.get("content", {}).get("name") if not schema_name: logger.warning("Schema without a name found, skipping.") @@ -180,7 +200,7 @@ async def register_schema_resources() -> None: ) ) - logger.info(f"Registered {len(schemas)} schema resources") + logger.info(f"Registered {len(all_schemas)} schema resources") except Exception as e: logger.warning(f"Failed to register schema resources: {e}") diff --git a/tests/test_client.py b/tests/test_client.py index 6367c00..19caf82 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -170,6 +170,8 @@ class TestCordraClient: {"name": "Document", "identifier": "test/doc-schema"}, ], "size": 3, + "pageNum": 0, + "pageSize": 20, } mock_response = mock_get.return_value mock_response.status_code = 200 @@ -178,21 +180,25 @@ class TestCordraClient: result = await client.find("type:Schema") - assert len(result) == 3 - assert result[0]["name"] == "User" - assert result[1]["name"] == "Project" - assert result[2]["name"] == "Document" + assert isinstance(result, dict) + assert len(result["results"]) == 3 + assert result["results"][0]["name"] == "User" + assert result["results"][1]["name"] == "Project" + assert result["results"][2]["name"] == "Document" + assert result["total_size"] == 3 + assert result["page_num"] == 0 + assert result["page_size"] == 20 mock_get.assert_called_once_with( "https://test.example.com/search", - params={"query": "type:Schema"}, + params={"query": "type:Schema", "pageSize": "20", "pageNum": "0"}, timeout=30, ) @patch("cordra_mcp.client.requests.Session.get") async def test_find_empty_results(self, mock_get, client): """Test find with empty results.""" - mock_response_data = {"results": [], "size": 0} + mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20} mock_response = mock_get.return_value mock_response.status_code = 200 mock_response.json.return_value = mock_response_data @@ -200,26 +206,17 @@ class TestCordraClient: result = await client.find("type:NonExistent") - assert result == [] + assert isinstance(result, dict) + assert result["results"] == [] + assert result["total_size"] == 0 + assert result["page_num"] == 0 + assert result["page_size"] == 20 mock_get.assert_called_once_with( "https://test.example.com/search", - params={"query": "type:NonExistent"}, + params={"query": "type:NonExistent", "pageSize": "20", "pageNum": "0"}, timeout=30, ) - @patch("cordra_mcp.client.requests.Session.get") - async def test_find_no_results_key(self, mock_get, client): - """Test find with response missing results key.""" - mock_response_data = {"size": 0} # No results key - mock_response = mock_get.return_value - mock_response.status_code = 200 - mock_response.json.return_value = mock_response_data - mock_response.raise_for_status.return_value = None - - result = await client.find("type:Schema") - - assert result == [] - @patch("cordra_mcp.client.requests.Session.get") async def test_find_error(self, mock_get, client): """Test find error handling.""" @@ -236,64 +233,108 @@ class TestCordraClient: @patch("cordra_mcp.client.requests.Session.get") async def test_find_with_type_filter(self, mock_get, client): """Test find operation with type filter constructs correct query.""" + mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20} mock_response = mock_get.return_value mock_response.status_code = 200 - mock_response.json.return_value = {"results": []} + mock_response.json.return_value = mock_response_data mock_response.ok = True await client.find("name:John", object_type="Person") mock_get.assert_called_once_with( "https://test.example.com/search", - params={"query": "type:Person AND (name:John)"}, + params={"query": "type:Person AND (name:John)", "pageSize": "20", "pageNum": "0"}, timeout=30, ) @patch("cordra_mcp.client.requests.Session.get") - async def test_find_with_limit(self, mock_get, client): - """Test find operation with limit adds pageSize parameter.""" + async def test_find_with_page_size(self, mock_get, client): + """Test find operation with custom page size.""" + mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 50} mock_response = mock_get.return_value mock_response.status_code = 200 - mock_response.json.return_value = {"results": []} + mock_response.json.return_value = mock_response_data mock_response.ok = True - await client.find("type:Test", limit=50) + await client.find("type:Test", page_size=50) mock_get.assert_called_once_with( "https://test.example.com/search", - params={"query": "type:Test", "pageSize": "50"}, + params={"query": "type:Test", "pageSize": "50", "pageNum": "0"}, timeout=30, ) @patch("cordra_mcp.client.requests.Session.get") - async def test_find_with_type_and_limit(self, mock_get, client): - """Test find operation with both type filter and limit.""" + async def test_find_with_type_and_page_size(self, mock_get, client): + """Test find operation with both type filter and page size.""" + mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 25} mock_response = mock_get.return_value mock_response.status_code = 200 - mock_response.json.return_value = {"results": []} + mock_response.json.return_value = mock_response_data mock_response.ok = True - await client.find("title:Report", object_type="Document", limit=25) + await client.find("title:Report", object_type="Document", page_size=25) mock_get.assert_called_once_with( "https://test.example.com/search", - params={"query": "type:Document AND (title:Report)", "pageSize": "25"}, + params={"query": "type:Document AND (title:Report)", "pageSize": "25", "pageNum": "0"}, timeout=30, ) @patch("cordra_mcp.client.requests.Session.get") - async def test_find_no_optional_params(self, mock_get, client): - """Test find operation with no optional parameters.""" + async def test_find_default_params(self, mock_get, client): + """Test find operation with default parameters.""" + mock_response_data = {"results": [], "size": 0, "pageNum": 0, "pageSize": 20} mock_response = mock_get.return_value mock_response.status_code = 200 - mock_response.json.return_value = {"results": []} + mock_response.json.return_value = mock_response_data mock_response.ok = True await client.find("content:test") mock_get.assert_called_once_with( "https://test.example.com/search", - params={"query": "content:test"}, + params={"query": "content:test", "pageSize": "20", "pageNum": "0"}, + timeout=30, + ) + + @patch("cordra_mcp.client.requests.Session.get") + async def test_find_with_page_num(self, mock_get, client): + """Test find operation with specific page number.""" + mock_response_data = {"results": [], "size": 100, "pageNum": 2, "pageSize": 20} + mock_response = mock_get.return_value + mock_response.status_code = 200 + mock_response.json.return_value = mock_response_data + mock_response.ok = True + + result = await client.find("type:Schema", page_num=2) + + assert result["page_num"] == 2 + assert result["page_size"] == 20 + assert result["total_size"] == 100 + mock_get.assert_called_once_with( + "https://test.example.com/search", + params={"query": "type:Schema", "pageSize": "20", "pageNum": "2"}, + timeout=30, + ) + + @patch("cordra_mcp.client.requests.Session.get") + async def test_find_with_custom_page_size_and_num(self, mock_get, client): + """Test find operation with custom page size and page number.""" + mock_response_data = {"results": [], "size": 500, "pageNum": 5, "pageSize": 10} + mock_response = mock_get.return_value + mock_response.status_code = 200 + mock_response.json.return_value = mock_response_data + mock_response.ok = True + + result = await client.find("type:Document", page_size=10, page_num=5) + + assert result["page_num"] == 5 + assert result["page_size"] == 10 + assert result["total_size"] == 500 + mock_get.assert_called_once_with( + "https://test.example.com/search", + params={"query": "type:Document", "pageSize": "10", "pageNum": "5"}, timeout=30, ) @@ -372,6 +413,5 @@ class TestCordraConfig: assert config.base_url == "https://localhost:8443" assert config.username is None assert config.password is None - assert config.max_search_results == 1000 assert config.verify_ssl is True assert config.timeout == 30 diff --git a/tests/test_server.py b/tests/test_server.py index af254e7..4a9f171 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -190,12 +190,17 @@ class TestSchemaResourceFunctions: @patch('cordra_mcp.server.cordra_client') async def test_register_schema_resources_success(self, mock_client): """Test successful schema resource registration.""" - mock_schemas = [ - {"content": {"name": "User"}, "id": "test/user-schema"}, - {"content": {"name": "Project"}, "id": "test/project-schema"}, - {"content": {"name": "Document"}, "id": "test/doc-schema"} - ] - mock_client.find = AsyncMock(return_value=mock_schemas) + mock_search_result = { + "results": [ + {"content": {"name": "User"}, "id": "test/user-schema"}, + {"content": {"name": "Project"}, "id": "test/project-schema"}, + {"content": {"name": "Document"}, "id": "test/doc-schema"} + ], + "total_size": 3, + "page_num": 0, + "page_size": 20 + } + mock_client.find = AsyncMock(return_value=mock_search_result) # Mock the mcp.add_resource method with patch('cordra_mcp.server.mcp') as mock_mcp: @@ -203,7 +208,7 @@ class TestSchemaResourceFunctions: await register_schema_resources() # Verify the client was called with correct query - mock_client.find.assert_called_once_with("type:Schema") + mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0) # Verify add_resource was called for each schema assert mock_mcp.add_resource.call_count == 3 @@ -211,12 +216,17 @@ class TestSchemaResourceFunctions: @patch('cordra_mcp.server.cordra_client') async def test_register_schema_resources_missing_name(self, mock_client): """Test schema resource registration with objects missing name field.""" - mock_schemas = [ - {"content": {"name": "User"}, "id": "test/user-schema"}, - {"content": {}, "id": "test/no-name-schema"}, # Missing name field - {"content": {"name": "Project"}, "id": "test/project-schema"} - ] - mock_client.find = AsyncMock(return_value=mock_schemas) + mock_search_result = { + "results": [ + {"content": {"name": "User"}, "id": "test/user-schema"}, + {"content": {}, "id": "test/no-name-schema"}, # Missing name field + {"content": {"name": "Project"}, "id": "test/project-schema"} + ], + "total_size": 3, + "page_num": 0, + "page_size": 20 + } + mock_client.find = AsyncMock(return_value=mock_search_result) with patch('cordra_mcp.server.mcp') as mock_mcp: from cordra_mcp.server import register_schema_resources @@ -234,22 +244,63 @@ class TestSchemaResourceFunctions: from cordra_mcp.server import register_schema_resources await register_schema_resources() # Should complete without raising - mock_client.find.assert_called_once_with("type:Schema") + mock_client.find.assert_called_once_with("type:Schema", page_size=20, page_num=0) + + @patch('cordra_mcp.server.cordra_client') + async def test_register_schema_resources_pagination(self, mock_client): + """Test schema resource registration with pagination.""" + # Mock multiple pages of results + # First page with full 20 results (simulating more schemas) + first_page_schemas = [{"content": {"name": f"Schema{i}"}, "id": f"test/schema{i}"} for i in range(20)] + first_page = { + "results": first_page_schemas, + "total_size": 25, + "page_num": 0, + "page_size": 20 + } + + # Second page with fewer results (indicating last page) + second_page = { + "results": [ + {"content": {"name": "Document"}, "id": "test/doc-schema"}, + ], + "total_size": 25, + "page_num": 1, + "page_size": 20 + } + + # Return first page, then second page (with fewer results indicating last page) + mock_client.find = AsyncMock(side_effect=[first_page, second_page]) + + with patch('cordra_mcp.server.mcp') as mock_mcp: + from cordra_mcp.server import register_schema_resources + await register_schema_resources() + + # Verify pagination calls + assert mock_client.find.call_count == 2 + mock_client.find.assert_any_call("type:Schema", page_size=20, page_num=0) + mock_client.find.assert_any_call("type:Schema", page_size=20, page_num=1) + + # Verify all 21 schemas were registered (20 from first page + 1 from second page) + assert mock_mcp.add_resource.call_count == 21 class TestSearchObjects: """Test the search_objects tool.""" @patch('cordra_mcp.server.cordra_client') - @patch('cordra_mcp.server.config') - async def test_search_objects_success(self, mock_config, mock_client): + async def test_search_objects_success(self, mock_client): """Test successful object search.""" - mock_config.max_search_results = 1000 - mock_results = [ - {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, - {"id": "people/jane-smith", "type": "Person", "content": {"name": "Jane Smith"}}, - ] - mock_client.find = AsyncMock(return_value=mock_results) + mock_search_result = { + "results": [ + {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, + {"id": "people/jane-smith", "type": "Person", "content": {"name": "Jane Smith"}}, + ], + "total_size": 2, + "page_num": 0, + "page_size": 1000 + } + mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("name:John") @@ -260,17 +311,20 @@ 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, limit=1000) + mock_client.find.assert_called_once_with("name:John", object_type=None, page_size=1, page_num=0) @patch('cordra_mcp.server.cordra_client') - @patch('cordra_mcp.server.config') - async def test_search_objects_with_type_filter(self, mock_config, mock_client): + async def test_search_objects_with_type_filter(self, mock_client): """Test object search with type filter.""" - mock_config.max_search_results = 1000 - mock_results = [ - {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, - ] - mock_client.find = AsyncMock(return_value=mock_results) + mock_search_result = { + "results": [ + {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, + ], + "total_size": 1, + "page_num": 0, + "page_size": 1000 + } + mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("name:John", type="Person") @@ -280,17 +334,20 @@ 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", limit=1000) + mock_client.find.assert_called_once_with("name:John", object_type="Person", page_size=1, page_num=0) @patch('cordra_mcp.server.cordra_client') - @patch('cordra_mcp.server.config') - async def test_search_objects_with_limit(self, mock_config, mock_client): + async def test_search_objects_with_limit(self, mock_client): """Test object search with custom limit.""" - mock_config.max_search_results = 1000 - mock_results = [ - {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, - ] - mock_client.find = AsyncMock(return_value=mock_results) + mock_search_result = { + "results": [ + {"id": "people/john-doe", "type": "Person", "content": {"name": "John Doe"}}, + ], + "total_size": 1, + "page_num": 0, + "page_size": 50 + } + mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("name:John", limit=50) @@ -299,17 +356,20 @@ 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, limit=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.config') - async def test_search_objects_with_all_parameters(self, mock_config, mock_client): + async def test_search_objects_with_all_parameters(self, mock_client): """Test object search with all parameters.""" - mock_config.max_search_results = 1000 - mock_results = [ - {"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}}, - ] - mock_client.find = AsyncMock(return_value=mock_results) + mock_search_result = { + "results": [ + {"id": "documents/report-123", "type": "Document", "content": {"title": "Report"}}, + ], + "total_size": 1, + "page_num": 0, + "page_size": 25 + } + mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("title:Report", type="Document", limit=25) @@ -319,14 +379,18 @@ 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", limit=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.config') - async def test_search_objects_empty_results(self, mock_config, mock_client): + async def test_search_objects_empty_results(self, mock_client): """Test object search with no results.""" - mock_config.max_search_results = 1000 - mock_client.find = AsyncMock(return_value=[]) + mock_search_result = { + "results": [], + "total_size": 0, + "page_num": 0, + "page_size": 1000 + } + mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("nonexistent:data") @@ -334,43 +398,42 @@ class TestSearchObjects: parsed_result = json.loads(result) assert parsed_result == [] - mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, limit=1000) + mock_client.find.assert_called_once_with("nonexistent:data", object_type=None, page_size=1, page_num=0) @patch('cordra_mcp.server.cordra_client') - @patch('cordra_mcp.server.config') - async def test_search_objects_client_error(self, mock_config, mock_client): + async def test_search_objects_client_error(self, mock_client): """Test object search with client error.""" - mock_config.max_search_results = 1000 mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed")) with pytest.raises(RuntimeError) as exc_info: 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, limit=1000) + mock_client.find.assert_called_once_with("test:query", object_type=None, page_size=1, page_num=0) @patch('cordra_mcp.server.cordra_client') - @patch('cordra_mcp.server.config') - async def test_search_objects_value_error(self, mock_config, mock_client): + async def test_search_objects_value_error(self, mock_client): """Test object search with value error.""" - mock_config.max_search_results = 1000 mock_client.find = AsyncMock(side_effect=ValueError("Invalid query")) with pytest.raises(RuntimeError) as exc_info: 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, limit=1000) + mock_client.find.assert_called_once_with("invalid:query", object_type=None, page_size=1, page_num=0) @patch('cordra_mcp.server.cordra_client') - @patch('cordra_mcp.server.config') - async def test_search_objects_json_formatting(self, mock_config, mock_client): + async def test_search_objects_json_formatting(self, mock_client): """Test that search results are properly formatted as JSON.""" - mock_config.max_search_results = 1000 - mock_results = [ - {"id": "test/object", "type": "Test", "content": {"data": "value"}}, - ] - mock_client.find = AsyncMock(return_value=mock_results) + mock_search_result = { + "results": [ + {"id": "test/object", "type": "Test", "content": {"data": "value"}}, + ], + "total_size": 1, + "page_num": 0, + "page_size": 1000 + } + mock_client.find = AsyncMock(return_value=mock_search_result) result = await search_objects("test:query") @@ -386,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=1, 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."""