5 Commits
v1.3.1 ... main

Author SHA1 Message Date
Daniel Bauer
8e6aa73f50 feat: bump version 2025-12-04 13:48:23 +01:00
Daniel Bauer
a4d12d526b feat: remove resources for schemas and replace them with tool calls 2025-12-04 13:47:42 +01:00
Daniel Bauer
ad6dce005b feat: remove cordra object resource and made design object retrieval a tool call 2025-12-04 13:40:57 +01:00
Daniel Bauer
d42bb1a581 feat: bump version 2025-12-03 16:14:07 +01:00
Daniel Bauer
c3b202c998 feat: fix versioning 2025-12-03 16:12:58 +01:00
6 changed files with 263 additions and 343 deletions

View File

@@ -18,25 +18,33 @@ ensuring safe exploration without risk of data modification or corruption.
## MCP Architecture ## MCP Architecture
### Resources
- `cordra://objects/{prefix}/{suffix}` - Retrieve a specific object by its handle identifier
- `cordra://schemas/{schema_name}` - Schema definition for a specific type.
- `cordra://design` - Design document containing the overall structure and configuration of the Cordra repository.
### Tools ### Tools
- `list_types` - List all available types in the Cordra repository.
- Returns a JSON array of type names that are defined in the repository
- Types are returned in sorted order
- `get_type_schema` - Retrieve the JSON schema definition for a specific type.
- `type_name` - The name of the type (e.g., "Person", "Document", "Project")
- Returns the full schema definition as JSON
- `get_object` - Retrieve a digital object by its complete ID/handle. - `get_object` - Retrieve a digital object by its complete ID/handle.
- `object_id` - Complete object ID (e.g., "test/abc123") - `object_id` - Complete object ID (e.g., "test/abc123")
- `search_objects` - Search for digital objects using a query string with pagination support. - `search_objects` - Search for digital objects using a query string with pagination support.
- `query` - Lucene/Solr compatible search query - `query` - Lucene/Solr compatible search query
- `type` - Optional filter by object type - `type` - Optional filter by object type
- `limit` - Number of results per page (default: 25) - `limit` - Number of results per page (default: 25)
- `page_num` - Page number to retrieve, 0-based (default: 0) - `page_num` - Page number to retrieve, 0-based (default: 0)
- `count_objects` - Count the total number of objects matching a query. - `count_objects` - Count the total number of objects matching a query.
- `query` - Lucene/Solr compatible search query - `query` - Lucene/Solr compatible search query
- `type` - Optional filter by object type - `type` - Optional filter by object type
- `get_design_object` - Retrieve the Cordra design object containing repository configuration.
- Includes type definitions, workflow configurations, and system settings
- Administrative privileges are typically required to access this object
#### Query Syntax #### Query Syntax
**CRITICAL**: JSON properties MUST be prefixed with `/` **CRITICAL**: JSON properties MUST be prefixed with `/`

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "cordra-mcp" name = "cordra-mcp"
version = "1.3.1.1" version = "1.4.0"
description = "MCP server for Cordra digital object repository" description = "MCP server for Cordra digital object repository"
authors = [ authors = [
{name = "Daniel Bauer", email = "github@dbauer.me"}, {name = "Daniel Bauer", email = "github@dbauer.me"},

View File

@@ -1,3 +1,3 @@
"""MCP server for Cordra digital object repository.""" """MCP server for Cordra digital object repository."""
__version__ = "1.3.1" __version__ = "1.4.0"

View File

@@ -1,11 +1,9 @@
"""MCP server for Cordra digital object repository.""" """MCP server for Cordra digital object repository."""
import asyncio
import json import json
import logging import logging
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.resources import FunctionResource
from . import __version__ from . import __version__
from .client import ( from .client import (
@@ -169,51 +167,18 @@ async def get_object(object_id: str) -> str:
raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e
@mcp.resource( @mcp.tool(
"cordra://objects/{prefix}/{suffix}", name="get_design_object",
name="cordra-object", title="Get Cordra Design Object",
title="Retrieve Cordra Digital Object", description="""
description="Retrieve a Digital Object and Metadata from Cordra by its ID/handle.", The design object is the central location where Cordra stores its configuration,
mime_type="application/json", including type definitions, workflow configurations, and system settings.
Administrative privileges are typically required to access this object.
Returns: The design object as JSON
""",
) )
async def get_cordra_object(prefix: str, suffix: str) -> str: async def get_cordra_design_object() -> str:
"""Retrieve a Cordra digital object by its ID.
Args:
prefix: The prefix part of the object ID (e.g., 'wildlive')
suffix: The suffix part of the object ID (e.g., '7a4b7b65f8bb155ad36d')
Returns:
JSON representation of the digital object
Raises:
RuntimeError: If the object is not found or there's an API error
"""
object_id = f"{prefix}/{suffix}"
try:
digital_object = await cordra_client.get_object(object_id)
object_dict = digital_object.model_dump()
return json.dumps(object_dict, indent=2)
except ValueError as e:
raise RuntimeError(f"Invalid parameters: {e}") from e
except CordraNotFoundError as e:
raise RuntimeError(f"Object not found: {object_id}") from e
except CordraAuthenticationError as e:
raise RuntimeError(f"Authentication failed: {e}") from e
except CordraClientError as e:
raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e
@mcp.resource(
"cordra://design",
name="cordra-design",
title="Retrieve Cordra Design Object",
description="Retrieve the Cordra design object containing repository configuration. Administrative privileges are typically required to access this object.",
mime_type="application/json",
)
async def get_cordra_design() -> str:
"""Retrieve the Cordra design object containing repository configuration. """Retrieve the Cordra design object containing repository configuration.
The design object is the central location where Cordra stores its configuration, The design object is the central location where Cordra stores its configuration,
@@ -239,25 +204,25 @@ async def get_cordra_design() -> str:
raise RuntimeError(f"Failed to retrieve design object: {e}") from e raise RuntimeError(f"Failed to retrieve design object: {e}") from e
async def create_schema_resource(schema_name: str) -> str: @mcp.tool(
"""Create content for a specific schema resource.""" name="list_types",
try: title="List Available Types",
schema_object = await cordra_client.get_schema(schema_name) description="""List all available object types in the Cordra repository.
schema_dict = schema_object.model_dump()
return json.dumps(schema_dict, indent=2)
except CordraNotFoundError as e:
raise RuntimeError(f"Schema not found: {schema_name}") from e
except CordraAuthenticationError as e:
raise RuntimeError(f"Authentication failed: {e}") from e
except CordraClientError as e:
raise RuntimeError(f"Failed to retrieve schema {schema_name}: {e}") from e
Returns a list of type names that are defined in the repository as json array.""",
)
async def list_types() -> str:
"""List all available types in the Cordra repository.
async def register_schema_resources() -> None: Returns:
"""Register individual schema resources dynamically.""" JSON string containing a list of type names
Raises:
RuntimeError: If there's an API error or authentication failure
"""
try: try:
# Get all available schemas using pagination # Get all available types using pagination
all_schemas = [] all_types = []
page_num = 0 page_num = 0
page_size = 20 page_size = 20
@@ -266,7 +231,11 @@ async def register_schema_resources() -> None:
"type:Schema", page_size=page_size, page_num=page_num "type:Schema", page_size=page_size, page_num=page_num
) )
schemas = search_result["results"] schemas = search_result["results"]
all_schemas.extend(schemas)
for schema in schemas:
type_name = schema.get("content", {}).get("name")
if type_name:
all_types.append(type_name)
# Check if we've retrieved all schemas # Check if we've retrieved all schemas
if len(schemas) < page_size: if len(schemas) < page_size:
@@ -274,45 +243,55 @@ async def register_schema_resources() -> None:
page_num += 1 page_num += 1
for schema in all_schemas: all_types.sort()
schema_name = schema.get("content", {}).get("name") return json.dumps(all_types, indent=2)
if not schema_name:
logger.warning("Schema without a name found, skipping.")
continue
logger.info(f"Registering schema resource for cordra type {schema_name}") except CordraAuthenticationError as e:
raise RuntimeError(f"Authentication failed: {e}") from e
async def schema_fn(name: str = schema_name) -> str: except CordraClientError as e:
return await create_schema_resource(name) raise RuntimeError(f"Failed to list types: {e}") from e
mcp.add_resource(
FunctionResource.from_function(
uri=f"cordra://schemas/{schema_name}",
fn=schema_fn,
name=f"cordra-type-schema-{schema_name}",
title=f"Cordra Type Schema: {schema_name}",
description=f"Retrieve the JSON schema for the Cordra Type {schema_name}",
mime_type="application/json",
)
)
logger.info(f"Registered {len(all_schemas)} schema resources")
except Exception as e:
logger.warning(f"Failed to register schema resources: {e}")
async def initialize_server() -> None: @mcp.tool(
"""Initialize server resources before starting.""" name="get_type_schema",
logger.info(f"Initializing Cordra MCP server v{__version__}...") title="Get Type Schema",
await register_schema_resources() description="""Retrieve the JSON schema definition for a specific type.
logger.info("Server initialization complete")
Args:
type_name: The name of the type (e.g., "Person", "Document", "Project")
Returns: The full schema definition as JSON""",
)
async def get_type_schema(type_name: str) -> str:
"""Retrieve the JSON schema definition for a specific object type.
Args:
type_name: The name of the type to retrieve the schema for
Returns:
JSON string containing the schema definition
Raises:
RuntimeError: If the type is not found, authentication fails, or there's an API error
"""
try:
schema_object = await cordra_client.get_schema(type_name)
schema_dict = schema_object.model_dump()
return json.dumps(schema_dict, indent=2)
except CordraNotFoundError as e:
raise RuntimeError(f"Type '{type_name}' not found") from e
except CordraAuthenticationError as e:
raise RuntimeError(f"Authentication failed: {e}") from e
except CordraClientError as e:
raise RuntimeError(
f"Failed to retrieve schema for type '{type_name}': {e}"
) from e
def main() -> None: def main() -> None:
"""Main entry point for the MCP server.""" """Main entry point for the MCP server."""
logger.info(f"Starting Cordra MCP server v{__version__}...")
if config.run_mode == "stdio": if config.run_mode == "stdio":
asyncio.run(initialize_server())
mcp.run() mcp.run()
else: else:
mcp.run(transport="streamable-http") mcp.run(transport="streamable-http")

View File

@@ -14,9 +14,10 @@ from cordra_mcp.client import (
) )
from cordra_mcp.server import ( from cordra_mcp.server import (
count_objects, count_objects,
get_cordra_design, get_cordra_design_object,
get_cordra_object,
get_object, get_object,
get_type_schema,
list_types,
search_objects, search_objects,
) )
@@ -45,124 +46,6 @@ def sample_digital_object() -> DigitalObject:
) )
class TestGetCordraObject:
"""Test the get_cordra_object resource handler."""
@patch("cordra_mcp.server.cordra_client")
async def test_get_object_success(
self, mock_client: Any, sample_digital_object: DigitalObject
) -> None:
"""Test successful object retrieval."""
mock_client.get_object = AsyncMock(return_value=sample_digital_object)
result = await get_cordra_object("people", "john-doe-123")
# Verify the result is valid JSON
parsed_result = json.loads(result)
assert parsed_result["id"] == "people/john-doe-123"
assert parsed_result["type"] == "Person"
assert parsed_result["content"]["name"] == "John Doe"
assert parsed_result["content"]["birthday"] == "1990-05-15"
assert parsed_result["metadata"]["created"] == "2023-01-01"
assert len(parsed_result["payloads"]) == 1
assert parsed_result["payloads"][0]["name"] == "profile_photo"
# Verify the client was called with the correct object ID
mock_client.get_object.assert_called_once_with("people/john-doe-123")
@patch("cordra_mcp.server.cordra_client")
async def test_get_object_not_found(self, mock_client: Any) -> None:
"""Test object not found exception."""
mock_client.get_object = AsyncMock(
side_effect=CordraNotFoundError("Object not found: people/nonexistent")
)
with pytest.raises(RuntimeError) as exc_info:
await get_cordra_object("people", "nonexistent")
assert "Object not found: people/nonexistent" in str(exc_info.value)
mock_client.get_object.assert_called_once_with("people/nonexistent")
@patch("cordra_mcp.server.cordra_client")
async def test_get_object_client_error(self, mock_client: Any) -> None:
"""Test general client error handling."""
mock_client.get_object = AsyncMock(
side_effect=CordraClientError("Connection failed")
)
with pytest.raises(RuntimeError) as exc_info:
await get_cordra_object("people", "john-doe-123")
assert "Failed to retrieve object people/john-doe-123" in str(exc_info.value)
assert "Connection failed" in str(exc_info.value)
mock_client.get_object.assert_called_once_with("people/john-doe-123")
@patch("cordra_mcp.server.cordra_client")
async def test_object_id_construction(
self, mock_client: Any, sample_digital_object: DigitalObject
) -> None:
"""Test that object ID is correctly constructed from prefix and suffix."""
mock_client.get_object = AsyncMock(return_value=sample_digital_object)
# Test various prefix/suffix combinations
test_cases = [
("people", "john-doe-123", "people/john-doe-123"),
("documents", "report-2023", "documents/report-2023"),
("items", "item_with_underscores", "items/item_with_underscores"),
]
for prefix, suffix, expected_id in test_cases:
await get_cordra_object(prefix, suffix)
mock_client.get_object.assert_called_with(expected_id)
@patch("cordra_mcp.server.cordra_client")
async def test_json_formatting(
self, mock_client: Any, sample_digital_object: DigitalObject
) -> None:
"""Test that the returned JSON is properly formatted."""
mock_client.get_object = AsyncMock(return_value=sample_digital_object)
result = await get_cordra_object("people", "john-doe-123")
# Verify it's valid JSON with proper indentation
parsed_result = json.loads(result)
assert isinstance(parsed_result, dict)
# Check that the result contains indentation (pretty-printed)
assert " " in result # Should have 2-space indentation
# Verify all expected fields are present
assert "id" in parsed_result
assert "type" in parsed_result
assert "content" in parsed_result
assert "metadata" in parsed_result
assert "acl" in parsed_result
assert "payloads" in parsed_result
@patch("cordra_mcp.server.cordra_client")
async def test_minimal_object(self, mock_client: Any) -> None:
"""Test handling of object with minimal data."""
minimal_object = DigitalObject(
id="test/minimal",
type="",
content={"id": "test/minimal"},
metadata=None,
acl=None,
payloads=None,
)
mock_client.get_object = AsyncMock(return_value=minimal_object)
result = await get_cordra_object("test", "minimal")
parsed_result = json.loads(result)
assert parsed_result["id"] == "test/minimal"
assert parsed_result["type"] == ""
assert parsed_result["content"]["id"] == "test/minimal"
assert parsed_result["metadata"] is None
assert parsed_result["acl"] is None
assert parsed_result["payloads"] is None
class TestGetObject: class TestGetObject:
"""Test the get_object tool.""" """Test the get_object tool."""
@@ -224,12 +107,137 @@ class TestGetObject:
mock_client.get_object.assert_called_once_with("test/obj123") mock_client.get_object.assert_called_once_with("test/obj123")
class TestSchemaResourceFunctions: class TestListTypes:
"""Test the schema resource functions.""" """Test the list_types tool."""
@patch("cordra_mcp.server.cordra_client") @patch("cordra_mcp.server.cordra_client")
async def test_create_schema_resource_success(self, mock_client: Any) -> None: async def test_list_types_success(self, mock_client: Any) -> None:
"""Test successful schema resource creation.""" """Test successful listing of available types."""
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)
result = await list_types()
# Verify the result is valid JSON
parsed_result = json.loads(result)
assert parsed_result == ["Document", "Project", "User"] # Should be sorted
# Verify the client was called with correct query
mock_client.find.assert_called_once_with(
"type:Schema", page_size=20, page_num=0
)
@patch("cordra_mcp.server.cordra_client")
async def test_list_types_with_pagination(self, mock_client: Any) -> None:
"""Test listing types with pagination."""
first_page = {
"results": [
{"content": {"name": f"Type{i}"}, "id": f"test/schema{i}"}
for i in range(20)
],
"total_size": 25,
"page_num": 0,
"page_size": 20,
}
second_page = {
"results": [
{"content": {"name": "ZType"}, "id": "test/zschema"},
],
"total_size": 25,
"page_num": 1,
"page_size": 20,
}
mock_client.find = AsyncMock(side_effect=[first_page, second_page])
result = await list_types()
parsed_result = json.loads(result)
# Should contain all 21 types and be sorted
assert len(parsed_result) == 21
assert parsed_result == sorted(parsed_result)
assert "Type0" in parsed_result
assert "ZType" in parsed_result
# Verify pagination calls
assert mock_client.find.call_count == 2
@patch("cordra_mcp.server.cordra_client")
async def test_list_types_missing_name(self, mock_client: Any) -> None:
"""Test listing types when some schemas have missing name field."""
mock_search_result = {
"results": [
{"content": {"name": "User"}, "id": "test/user-schema"},
{"content": {}, "id": "test/no-name-schema"}, # Missing name
{"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)
result = await list_types()
# Only 2 types should be returned (those with name)
parsed_result = json.loads(result)
assert parsed_result == ["Project", "User"]
@patch("cordra_mcp.server.cordra_client")
async def test_list_types_client_error(self, mock_client: Any) -> None:
"""Test listing types with client error."""
mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed"))
with pytest.raises(RuntimeError) as exc_info:
await list_types()
assert "Failed to list types:" in str(exc_info.value)
@patch("cordra_mcp.server.cordra_client")
async def test_list_types_authentication_error(self, mock_client: Any) -> None:
"""Test listing types with authentication error."""
mock_client.find = AsyncMock(
side_effect=CordraAuthenticationError("Authentication failed")
)
with pytest.raises(RuntimeError) as exc_info:
await list_types()
assert "Authentication failed:" in str(exc_info.value)
@patch("cordra_mcp.server.cordra_client")
async def test_list_types_empty(self, mock_client: Any) -> None:
"""Test listing types when no types are available."""
mock_search_result = {
"results": [],
"total_size": 0,
"page_num": 0,
"page_size": 20,
}
mock_client.find = AsyncMock(return_value=mock_search_result)
result = await list_types()
parsed_result = json.loads(result)
assert parsed_result == []
class TestGetTypeSchema:
"""Test the get_type_schema tool."""
@patch("cordra_mcp.server.cordra_client")
async def test_get_type_schema_success(self, mock_client: Any) -> None:
"""Test successful schema retrieval."""
mock_schema = DigitalObject( mock_schema = DigitalObject(
id="test/user-schema", id="test/user-schema",
type="Schema", type="Schema",
@@ -237,9 +245,7 @@ class TestSchemaResourceFunctions:
) )
mock_client.get_schema = AsyncMock(return_value=mock_schema) mock_client.get_schema = AsyncMock(return_value=mock_schema)
from cordra_mcp.server import create_schema_resource result = await get_type_schema("User")
result = await create_schema_resource("User")
# Verify the result is valid JSON # Verify the result is valid JSON
parsed_result = json.loads(result) parsed_result = json.loads(result)
@@ -251,131 +257,58 @@ class TestSchemaResourceFunctions:
mock_client.get_schema.assert_called_once_with("User") mock_client.get_schema.assert_called_once_with("User")
@patch("cordra_mcp.server.cordra_client") @patch("cordra_mcp.server.cordra_client")
async def test_create_schema_resource_not_found(self, mock_client: Any) -> None: async def test_get_type_schema_not_found(self, mock_client: Any) -> None:
"""Test schema resource creation with schema not found.""" """Test schema retrieval with type not found."""
mock_client.get_schema = AsyncMock( mock_client.get_schema = AsyncMock(
side_effect=CordraNotFoundError("Schema not found") side_effect=CordraNotFoundError("Schema not found")
) )
from cordra_mcp.server import create_schema_resource
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
await create_schema_resource("NonExistent") await get_type_schema("NonExistent")
assert "Schema not found: NonExistent" in str(exc_info.value) assert "Type 'NonExistent' not found" in str(exc_info.value)
mock_client.get_schema.assert_called_once_with("NonExistent") mock_client.get_schema.assert_called_once_with("NonExistent")
@patch("cordra_mcp.server.cordra_client") @patch("cordra_mcp.server.cordra_client")
async def test_register_schema_resources_success(self, mock_client: Any) -> None: async def test_get_type_schema_authentication_error(self, mock_client: Any) -> None:
"""Test successful schema resource registration.""" """Test schema retrieval with authentication error."""
mock_search_result = { mock_client.get_schema = AsyncMock(
"results": [ side_effect=CordraAuthenticationError("Authentication failed")
{"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:
from cordra_mcp.server import register_schema_resources
await register_schema_resources()
# Verify the client was called with correct query
mock_client.find.assert_called_once_with(
"type:Schema", page_size=20, page_num=0
) )
# Verify add_resource was called for each schema with pytest.raises(RuntimeError) as exc_info:
assert mock_mcp.add_resource.call_count == 3 await get_type_schema("User")
assert "Authentication failed:" in str(exc_info.value)
@patch("cordra_mcp.server.cordra_client") @patch("cordra_mcp.server.cordra_client")
async def test_register_schema_resources_missing_name( async def test_get_type_schema_client_error(self, mock_client: Any) -> None:
self, mock_client: Any """Test schema retrieval with client error."""
) -> None: mock_client.get_schema = AsyncMock(
"""Test schema resource registration with objects missing name field.""" side_effect=CordraClientError("Connection failed")
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
await register_schema_resources()
# Only 2 schemas should be registered (those with name field)
assert mock_mcp.add_resource.call_count == 2
@patch("cordra_mcp.server.cordra_client")
async def test_register_schema_resources_client_error(
self, mock_client: Any
) -> None:
"""Test schema resource registration with client error."""
mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed"))
# Should not raise an exception, just log a warning
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", page_size=20, page_num=0
) )
with pytest.raises(RuntimeError) as exc_info:
await get_type_schema("User")
assert "Failed to retrieve schema for type 'User':" in str(exc_info.value)
@patch("cordra_mcp.server.cordra_client") @patch("cordra_mcp.server.cordra_client")
async def test_register_schema_resources_pagination(self, mock_client: Any) -> None: async def test_get_type_schema_json_formatting(self, mock_client: Any) -> None:
"""Test schema resource registration with pagination.""" """Test that schema is properly formatted as JSON."""
# Mock multiple pages of results mock_schema = DigitalObject(
# First page with full 20 results (simulating more schemas) id="test/schema",
first_page_schemas = [ type="Schema",
{"content": {"name": f"Schema{i}"}, "id": f"test/schema{i}"} content={"name": "Test", "properties": {"field": "value"}},
for i in range(20) )
] mock_client.get_schema = AsyncMock(return_value=mock_schema)
first_page = {
"results": first_page_schemas,
"total_size": 25,
"page_num": 0,
"page_size": 20,
}
# Second page with fewer results (indicating last page) result = await get_type_schema("Test")
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) # Verify it's valid JSON with proper indentation
mock_client.find = AsyncMock(side_effect=[first_page, second_page]) parsed_result = json.loads(result)
assert isinstance(parsed_result, dict)
with patch("cordra_mcp.server.mcp") as mock_mcp: assert " " in result # Should have 2-space indentation
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: class TestSearchObjects:
@@ -663,7 +596,7 @@ class TestSearchObjects:
class TestGetCordraDesign: class TestGetCordraDesign:
"""Test the get_cordra_design resource handler.""" """Test the get_cordra_design_object tool."""
@patch("cordra_mcp.server.cordra_client") @patch("cordra_mcp.server.cordra_client")
async def test_get_design_success(self, mock_client: Any) -> None: async def test_get_design_success(self, mock_client: Any) -> None:
@@ -680,7 +613,7 @@ class TestGetCordraDesign:
) )
mock_client.get_design = AsyncMock(return_value=mock_design) mock_client.get_design = AsyncMock(return_value=mock_design)
result = await get_cordra_design() result = await get_cordra_design_object()
# Verify the result is valid JSON # Verify the result is valid JSON
parsed_result = json.loads(result) parsed_result = json.loads(result)
@@ -701,7 +634,7 @@ class TestGetCordraDesign:
) )
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
await get_cordra_design() await get_cordra_design_object()
assert "Design object not found" in str(exc_info.value) assert "Design object not found" in str(exc_info.value)
mock_client.get_design.assert_called_once() mock_client.get_design.assert_called_once()
@@ -714,7 +647,7 @@ class TestGetCordraDesign:
) )
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
await get_cordra_design() await get_cordra_design_object()
assert "Authentication failed" in str(exc_info.value) assert "Authentication failed" in str(exc_info.value)
mock_client.get_design.assert_called_once() mock_client.get_design.assert_called_once()
@@ -727,7 +660,7 @@ class TestGetCordraDesign:
) )
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(RuntimeError) as exc_info:
await get_cordra_design() await get_cordra_design_object()
assert "Failed to retrieve design object" in str(exc_info.value) assert "Failed to retrieve design object" in str(exc_info.value)
assert "Connection failed" in str(exc_info.value) assert "Connection failed" in str(exc_info.value)
@@ -744,7 +677,7 @@ class TestGetCordraDesign:
) )
mock_client.get_design = AsyncMock(return_value=mock_design) mock_client.get_design = AsyncMock(return_value=mock_design)
result = await get_cordra_design() result = await get_cordra_design_object()
# Verify it's valid JSON with proper indentation # Verify it's valid JSON with proper indentation
parsed_result = json.loads(result) parsed_result = json.loads(result)

2
uv.lock generated
View File

@@ -113,7 +113,7 @@ wheels = [
[[package]] [[package]]
name = "cordra-mcp" name = "cordra-mcp"
version = "1.3.1.1" version = "1.4.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "mcp", extra = ["cli"] }, { name = "mcp", extra = ["cli"] },