Skip to main content
Glama

CEDAR MCP Server

test_server.py24.1 kB
#!/usr/bin/env python3 import pytest from unittest.mock import patch import requests from src.cedar_mcp.external_api import search_instance_ids, get_instance import sys import io import os @pytest.mark.integration class TestGetTemplate: """Integration tests for get_template function from server.py.""" def test_get_template_valid_id( self, cedar_api_key: str, sample_cedar_template_id: str ): """Test fetching a valid template from CEDAR.""" # We need to test the actual MCP tool function # Since it's defined inside main(), we'll test by importing and running # Mock sys.argv to provide API keys with patch.object(sys, "argv", ["server.py", "--cedar-api-key", cedar_api_key]): # Capture stdout to prevent MCP server from running io.StringIO() # We'll test the function logic by extracting it # Since the function is defined inside main(), we need to test indirectly headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } from urllib.parse import quote encoded_template_id = quote(sample_cedar_template_id, safe="") base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) try: response = requests.get(base_url, headers=headers, timeout=30) response.raise_for_status() template_data = response.json() # Should receive valid JSON-LD data assert isinstance(template_data, dict) assert "@context" in template_data or "properties" in template_data # Test that cleaning function works with real data from src.cedar_mcp.processing import clean_template_response from dotenv import load_dotenv load_dotenv(".env.test") bioportal_api_key = os.getenv("BIOPORTAL_API_KEY") cleaned_data = clean_template_response(template_data, bioportal_api_key) # Verify cleaned response structure assert isinstance(cleaned_data, dict) assert cleaned_data["type"] == "template" assert "name" in cleaned_data assert "children" in cleaned_data assert isinstance(cleaned_data["children"], list) except requests.exceptions.RequestException as e: pytest.fail(f"Failed to fetch template: {str(e)}") def test_get_template_invalid_id(self, cedar_api_key: str): """Test fetching with invalid template ID.""" headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } from urllib.parse import quote invalid_template_id = ( "https://repo.metadatacenter.org/templates/nonexistent-template-id" ) encoded_template_id = quote(invalid_template_id, safe="") base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) response = requests.get(base_url, headers=headers) # Should get 404 or other error status assert response.status_code != 200 def test_get_template_invalid_api_key(self, sample_cedar_template_id: str): """Test fetching with invalid API key.""" headers = { "Accept": "application/json", "Authorization": "apiKey invalid-key-12345", } from urllib.parse import quote encoded_template_id = quote(sample_cedar_template_id, safe="") base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) response = requests.get(base_url, headers=headers) # Should get 401 Unauthorized assert response.status_code == 401 def test_cedar_api_endpoint_structure(self): """Test CEDAR API endpoint URL structure and parameters.""" # Test URL encoding works correctly test_template_id = ( "https://repo.metadatacenter.org/templates/test-id-with-special-chars!" ) from urllib.parse import quote encoded_template_id = quote(test_template_id, safe="") # Should not contain unencoded special characters assert "!" not in encoded_template_id assert "%21" in encoded_template_id base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) # Test that URL is well-formed assert base_url.startswith("https://resource.metadatacenter.org/templates/") @pytest.mark.slow def test_cedar_template_real_data_structure( self, cedar_api_key: str, sample_cedar_template_id: str ): """Test real CEDAR template data structure and content.""" headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } from urllib.parse import quote encoded_template_id = quote(sample_cedar_template_id, safe="") base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) try: response = requests.get(base_url, headers=headers, timeout=30) response.raise_for_status() template_data = response.json() # Verify CEDAR JSON-LD structure assert isinstance(template_data, dict) # Common CEDAR template fields if "@context" in template_data: assert isinstance(template_data["@context"], dict) if "properties" in template_data: assert isinstance(template_data["properties"], dict) if "_ui" in template_data: assert isinstance(template_data["_ui"], dict) if "order" in template_data["_ui"]: assert isinstance(template_data["_ui"]["order"], list) # Should have schema metadata schema_fields = ["schema:name", "schema:description", "title"] has_schema_field = any(field in template_data for field in schema_fields) assert has_schema_field, "Template should have schema metadata" except requests.exceptions.RequestException as e: pytest.fail(f"Failed to fetch template for structure test: {str(e)}") def test_cedar_api_authentication_header(self, cedar_api_key: str): """Test CEDAR API authentication header format.""" headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } # Verify header format matches CEDAR documentation auth_header = headers["Authorization"] assert auth_header.startswith("apiKey ") assert len(auth_header.split(" ")) == 2 assert auth_header.split(" ")[1] == cedar_api_key @pytest.mark.unit class TestServerConfiguration: """Tests for server configuration and setup.""" def test_command_line_argument_parsing(self): """Test command line argument parsing.""" import argparse # Simulate the argument parser from server.py parser = argparse.ArgumentParser(description="CEDAR MCP Python Server") parser.add_argument("--cedar-api-key", type=str, help="CEDAR API key") parser.add_argument("--bioportal-api-key", type=str, help="BioPortal API key") # Test parsing with both arguments args = parser.parse_args( [ "--cedar-api-key", "test-cedar-key", "--bioportal-api-key", "test-bioportal-key", ] ) assert args.cedar_api_key == "test-cedar-key" assert args.bioportal_api_key == "test-bioportal-key" def test_api_key_validation_logic(self): """Test API key validation logic.""" # Test the logic used in main() for API key validation # Case 1: Command line argument provided command_line_key = "cmd-key" env_key = "env-key" result_key = command_line_key or env_key assert result_key == "cmd-key" # Case 2: Only environment variable provided command_line_key = None env_key = "env-key" result_key = command_line_key or env_key assert result_key == "env-key" # Case 3: Neither provided command_line_key = None env_key = None result_key = command_line_key or env_key assert result_key is None @pytest.mark.integration class TestServerEnvironment: """Integration tests for server environment setup.""" def test_environment_variable_loading(self): """Test that environment variables are loaded correctly.""" import os from pathlib import Path # First check if environment variables are already set (CI context) cedar_key = os.getenv("CEDAR_API_KEY") bioportal_key = os.getenv("BIOPORTAL_API_KEY") # If not set, try to load from .env.test (local development) if not cedar_key or not bioportal_key: env_file = Path(".env.test") if env_file.exists(): from dotenv import load_dotenv load_dotenv(".env.test") cedar_key = os.getenv("CEDAR_API_KEY") bioportal_key = os.getenv("BIOPORTAL_API_KEY") else: pytest.skip( "Neither environment variables nor .env.test file available" ) assert cedar_key is not None, "CEDAR_API_KEY not found" assert bioportal_key is not None, "BIOPORTAL_API_KEY not found" assert len(cedar_key) > 0, "CEDAR_API_KEY is empty" assert len(bioportal_key) > 0, "BIOPORTAL_API_KEY is empty" @pytest.mark.integration class TestEndToEndWorkflow: """End-to-end integration tests.""" def test_complete_template_processing_workflow( self, cedar_api_key: str, sample_cedar_template_id: str ): """Test complete workflow from CEDAR API to cleaned template.""" from dotenv import load_dotenv load_dotenv(".env.test") bioportal_api_key = os.getenv("BIOPORTAL_API_KEY") # Step 1: Fetch from CEDAR API headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } from urllib.parse import quote encoded_template_id = quote(sample_cedar_template_id, safe="") base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) try: response = requests.get(base_url, headers=headers, timeout=30) response.raise_for_status() raw_template = response.json() # Step 2: Clean and transform template from src.cedar_mcp.processing import clean_template_response cleaned_template = clean_template_response(raw_template, bioportal_api_key) # Step 3: Verify complete transformation assert isinstance(cleaned_template, dict) assert "type" in cleaned_template assert "name" in cleaned_template assert "children" in cleaned_template # Step 4: Verify fields are properly transformed if cleaned_template["children"]: for field in cleaned_template["children"]: assert "name" in field assert "description" in field assert "prefLabel" in field assert "datatype" in field assert "configuration" in field # Configuration should have required field assert "required" in field["configuration"] assert isinstance(field["configuration"]["required"], bool) except requests.exceptions.RequestException as e: pytest.fail(f"End-to-end workflow failed: {str(e)}") @pytest.mark.integration def test_complete_template_instance_processing_workflow( self, cedar_api_key: str, sample_cedar_template_instance_id: str ): """Test complete workflow from CEDAR API to cleaned template instance.""" # Step 1: Fetch template instance from CEDAR API headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } from urllib.parse import quote encoded_instance_id = quote(sample_cedar_template_instance_id, safe="") base_url = f"https://resource.metadatacenter.org/template-instances/{encoded_instance_id}" try: response = requests.get(base_url, headers=headers, timeout=30) response.raise_for_status() raw_instance = response.json() # Step 2: Clean and transform template instance from src.cedar_mcp.processing import clean_template_instance_response cleaned_instance = clean_template_instance_response(raw_instance) # Step 3: Verify complete transformation assert isinstance(cleaned_instance, dict) # Verify root-level metadata removal root_metadata_fields = { "@context", "schema:isBasedOn", "schema:name", "schema:description", "pav:createdOn", "pav:createdBy", "pav:derivedFrom", "oslc:modifiedBy", "@id", } for field in root_metadata_fields: assert field not in cleaned_instance, ( f"Root metadata field '{field}' should be removed" ) # Step 4: Verify nested @context removal and template-element-instance @id removal def check_nested_structure(obj, path=""): """Recursively check that @context and template-element-instance @ids are removed.""" if isinstance(obj, dict): # Check no @context fields exist assert "@context" not in obj, f"@context found at path: {path}" # Check template-element-instance @id removal if "@id" in obj: id_value = obj["@id"] assert "template-element-instances" not in id_value, ( f"template-element-instance @id found at path: {path}" ) # Check for proper @id -> iri transformation if "iri" in obj: # Verify it's a proper IRI and not a template-element-instance iri_value = obj["iri"] assert isinstance(iri_value, str), ( f"iri should be string at path: {path}" ) assert "template-element-instances" not in iri_value, ( f"template-element-instance iri found at path: {path}" ) # Check for proper rdfs:label -> label transformation assert "rdfs:label" not in obj, ( f"rdfs:label found at path: {path} (should be 'label')" ) # Recursively check nested objects for key, value in obj.items(): check_nested_structure(value, f"{path}.{key}" if path else key) elif isinstance(obj, list): # Recursively check list items for i, item in enumerate(obj): check_nested_structure( item, f"{path}[{i}]" if path else f"[{i}]" ) check_nested_structure(cleaned_instance) # Step 5: Verify @value flattening and type conversion def check_value_flattening_and_type_conversion(obj, path=""): """Recursively check that @value objects are properly flattened and type-converted.""" if isinstance(obj, dict): # Check for single-key @value objects (these should be flattened) if len(obj) == 1 and "@value" in obj: # This should not happen since these get flattened assert False, ( f"Single @value object found at path: {path} (should be flattened)" ) # Check that @value and @type objects are properly converted if "@value" in obj or "@type" in obj: assert False, ( f"@value or @type found at path: {path} (should be converted)" ) # Recursively check nested objects for key, value in obj.items(): check_value_flattening_and_type_conversion( value, f"{path}.{key}" if path else key ) elif isinstance(obj, list): # Recursively check list items for i, item in enumerate(obj): check_value_flattening_and_type_conversion( item, f"{path}[{i}]" if path else f"[{i}]" ) check_value_flattening_and_type_conversion(cleaned_instance) # Step 6: Verify specific type conversions from real CEDAR data # Based on the known sample template instance data if "Project duration" in cleaned_instance: # Should be converted to numeric assert isinstance(cleaned_instance["Project duration"], (int, float)) print( f"Project duration: {cleaned_instance['Project duration']} (type: {type(cleaned_instance['Project duration'])})" ) if "Start date" in cleaned_instance: # Should remain as string (xsd:date) assert isinstance(cleaned_instance["Start date"], str) print( f"Start date: {cleaned_instance['Start date']} (type: {type(cleaned_instance['Start date'])})" ) if "End date" in cleaned_instance: # Should remain as string (xsd:date) assert isinstance(cleaned_instance["End date"], str) print( f"End date: {cleaned_instance['End date']} (type: {type(cleaned_instance['End date'])})" ) # Step 7: Verify data integrity - check that important data fields are preserved # (This varies by template instance, but we can check for non-empty structure) assert len(cleaned_instance) > 0, "Cleaned instance should not be empty" # Log some sample fields for debugging (if needed) print(f"Cleaned instance has {len(cleaned_instance)} top-level fields") if cleaned_instance: sample_keys = list(cleaned_instance.keys())[:5] # First 5 keys print(f"Sample field names: {sample_keys}") except requests.exceptions.RequestException as e: pytest.fail(f"End-to-end template instance workflow failed: {str(e)}") @pytest.mark.slow def test_template_with_bioportal_integration( self, cedar_api_key: str, sample_cedar_template_id: str ): """Test template processing that requires BioPortal integration.""" from dotenv import load_dotenv load_dotenv(".env.test") bioportal_api_key = os.getenv("BIOPORTAL_API_KEY") # Fetch a real template headers = { "Accept": "application/json", "Authorization": f"apiKey {cedar_api_key}", } from urllib.parse import quote encoded_template_id = quote(sample_cedar_template_id, safe="") base_url = ( f"https://resource.metadatacenter.org/templates/{encoded_template_id}" ) try: response = requests.get(base_url, headers=headers, timeout=30) response.raise_for_status() raw_template = response.json() # Process with BioPortal integration from src.cedar_mcp.processing import clean_template_response cleaned_template = clean_template_response(raw_template, bioportal_api_key) # Look for fields that might have used BioPortal fields_with_values = [ field for field in cleaned_template.get("children", []) if field.get("values") is not None ] # If there are controlled term fields, verify they're properly structured for field in fields_with_values: for value in field["values"]: assert "label" in value assert isinstance(value["label"], str) assert len(value["label"].strip()) > 0 # IRI is optional (None for literals) if "iri" in value and value["iri"] is not None: assert isinstance(value["iri"], str) assert value["iri"].startswith("http") except requests.exceptions.RequestException as e: pytest.fail(f"BioPortal integration test failed: {str(e)}") @pytest.mark.integration class TestGetInstancesBasedOnTemplate: """Integration tests for the complete get_instances_based_on_template MCP tool.""" def test_get_instances_based_on_template_integration(self, cedar_api_key: str): """Test complete get_instances_based_on_template workflow with real API.""" template_id = "8b47bae6-db32-4b13-9d12-d012f0be9412" # Step 1: Search for instances search_result = search_instance_ids(template_id, cedar_api_key, limit=3) if "error" in search_result: pytest.skip(f"Search failed: {search_result['error']}") if not search_result["instance_ids"]: pytest.skip("No instances found for testing") # Step 2: Fetch first 2 instances only for testing test_instance_ids = search_result["instance_ids"][:2] instances = [] failed_instances = [] for instance_id in test_instance_ids: instance_content = get_instance(instance_id, cedar_api_key) if "error" in instance_content: failed_instances.append( {"instance_id": instance_id, "error": instance_content["error"]} ) else: instances.append(instance_content) # Verify results assert len(instances) + len(failed_instances) == len(test_instance_ids) # At least some should succeed assert len(instances) > 0, "No instances were successfully fetched" # Verify instance structure before cleaning for instance in instances: assert "@id" in instance assert "schema:isBasedOn" in instance assert ( instance["schema:isBasedOn"] == f"https://repo.metadatacenter.org/templates/{template_id}" ) # Test cleaning integration from src.cedar_mcp.processing import clean_template_instance_response cleaned_instances = [] for instance in instances: cleaned = clean_template_instance_response(instance) cleaned_instances.append(cleaned) # Verify cleaning worked metadata_fields = {"@context", "schema:isBasedOn", "schema:name", "@id"} for field in metadata_fields: assert field not in cleaned, f"Metadata field {field} should be removed" # Verify we have cleaned instances assert len(cleaned_instances) > 0 assert all(isinstance(inst, dict) for inst in cleaned_instances)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BACH-AI-Tools/cedar-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server