Skip to main content
Glama

USPTO Final Petition Decisions MCP Server

by john-walkoe
test_tiered_convenience_params.pyβ€’20.6 kB
""" Test script for tiered convenience parameters implementation in FPD MCP. This script tests that: 1. The _build_convenience_query helper function works correctly 2. Different tool types get appropriate parameter sets 3. Date validation works properly 4. The tiered approach respects progressive disclosure principles 5. Parameter validation and error handling work correctly 6. Query building produces correct results """ import sys import os import re from datetime import datetime from typing import Optional, Dict, Any, Tuple def validate_date_range(date_str: str) -> str: """Validate date string in YYYY-MM-DD format""" if not date_str: return None # Remove whitespace clean_date = date_str.strip() # If empty after stripping, return None if not clean_date: return None # Check format YYYY-MM-DD if not re.match(r'^\d{4}-\d{2}-\d{2}$', clean_date): raise ValueError("Date must be in YYYY-MM-DD format (e.g., '2024-01-01')") # Validate actual date values try: datetime.strptime(clean_date, '%Y-%m-%d') except ValueError: raise ValueError("Invalid date. Please check year, month, and day values.") # Check reasonable date range (1990 to current year + 5) year = int(clean_date[:4]) current_year = datetime.now().year if year < 1990 or year > current_year + 5: raise ValueError(f"Date year must be between 1990 and {current_year + 5}") return clean_date def validate_string_param(param_name: str, param_value: str, max_length: int = 200) -> str: """Validate string parameter input""" if not param_value: return None # Trim whitespace clean_value = param_value.strip() if not clean_value: return None # Check length limits if len(clean_value) > max_length: raise ValueError(f"{param_name} too long. Maximum {max_length} characters.") # Check for suspicious characters that might indicate injection attempts if re.search(r'[<>"\'\\\\/\x00-\x1f]', clean_value): raise ValueError(f"{param_name} contains invalid characters.") return clean_value def validate_application_number(app_number: str) -> str: """Validate and clean USPTO application number format""" if not app_number: return None # Remove whitespace and clean format clean_number = app_number.strip().replace("/", "").replace(" ", "") if not clean_number: return None # Basic length validation (USPTO application numbers are typically 8 digits) if len(clean_number) < 6 or len(clean_number) > 10: raise ValueError("Application number should be 6-10 digits") # Check if all characters are digits if not clean_number.isdigit(): raise ValueError("Application number should contain only digits") return clean_number def _build_convenience_query( query: str = "", # Core Identity & Party applicant_name: Optional[str] = None, application_number: Optional[str] = None, patent_number: Optional[str] = None, # Decision Filters decision_type: Optional[str] = None, deciding_office: Optional[str] = None, # Date Ranges petition_date_start: Optional[str] = None, petition_date_end: Optional[str] = None, decision_date_start: Optional[str] = None, decision_date_end: Optional[str] = None, # Balanced tier additional parameters petition_type_code: Optional[str] = None, art_unit: Optional[str] = None, technology_center: Optional[str] = None, prosecution_status: Optional[str] = None, entity_status: Optional[str] = None, # Control which parameters are allowed allow_balanced_params: bool = False ) -> Tuple[str, Dict[str, Any]]: """Build query string from convenience parameters Returns: tuple: (final_query_string, convenience_parameters_used) """ try: # Build query from convenience parameters query_parts = [] convenience_params_used = {} # Include base query if provided if query and query.strip(): query_parts.append(f"({query})") convenience_params_used["base_query"] = query # Add minimal tier convenience parameters if applicant_name: validated_name = validate_string_param("applicant_name", applicant_name) if validated_name: query_parts.append(f'firstApplicantName:"{validated_name}"') convenience_params_used["applicant_name"] = validated_name if application_number: validated_app = validate_application_number(application_number) if validated_app: query_parts.append(f"applicationNumberText:{validated_app}") convenience_params_used["application_number"] = validated_app if patent_number: validated_patent = validate_string_param("patent_number", patent_number, 15) if validated_patent: query_parts.append(f"patentNumber:{validated_patent}") convenience_params_used["patent_number"] = validated_patent if decision_type: validated_decision = validate_string_param("decision_type", decision_type, 50) if validated_decision: query_parts.append(f"decisionTypeCodeDescriptionText:{validated_decision}") convenience_params_used["decision_type"] = validated_decision if deciding_office: validated_office = validate_string_param("deciding_office", deciding_office) if validated_office: query_parts.append(f'finalDecidingOfficeName:"{validated_office}"') convenience_params_used["deciding_office"] = validated_office # Date range filters if petition_date_start or petition_date_end: start = validate_date_range(petition_date_start) if petition_date_start else "*" end = validate_date_range(petition_date_end) if petition_date_end else "*" if start != "*" or end != "*": query_parts.append(f"petitionMailDate:[{start} TO {end}]") convenience_params_used["petition_date_range"] = f"{start} TO {end}" if decision_date_start or decision_date_end: start = validate_date_range(decision_date_start) if decision_date_start else "*" end = validate_date_range(decision_date_end) if decision_date_end else "*" if start != "*" or end != "*": query_parts.append(f"decisionDate:[{start} TO {end}]") convenience_params_used["decision_date_range"] = f"{start} TO {end}" # Add balanced tier additional parameters (only if allowed) if allow_balanced_params: if petition_type_code: validated_type = validate_string_param("petition_type_code", petition_type_code, 10) if validated_type: query_parts.append(f"decisionPetitionTypeCode:{validated_type}") convenience_params_used["petition_type_code"] = validated_type if art_unit: validated_art_unit = validate_string_param("art_unit", art_unit, 10) if validated_art_unit: query_parts.append(f"groupArtUnitNumber:{validated_art_unit}") convenience_params_used["art_unit"] = validated_art_unit if technology_center: validated_tc = validate_string_param("technology_center", technology_center, 10) if validated_tc: query_parts.append(f"technologyCenter:{validated_tc}") convenience_params_used["technology_center"] = validated_tc if prosecution_status: validated_status = validate_string_param("prosecution_status", prosecution_status) if validated_status: query_parts.append(f'prosecutionStatusCodeDescriptionText:"{validated_status}"') convenience_params_used["prosecution_status"] = validated_status if entity_status: validated_entity = validate_string_param("entity_status", entity_status, 50) if validated_entity: query_parts.append(f'businessEntityStatusCategory:"{validated_entity}"') convenience_params_used["entity_status"] = validated_entity else: # Check if balanced-only parameters were provided but not allowed balanced_only_params = [petition_type_code, art_unit, technology_center, prosecution_status, entity_status] provided_balanced_params = [p for p in balanced_only_params if p is not None] if provided_balanced_params: raise ValueError( "Parameters petition_type_code, art_unit, technology_center, prosecution_status, " "and entity_status are only available in fpd_search_petitions_balanced. " "Use fpd_search_petitions_balanced for advanced filtering." ) # Validate we have at least one search criterion if not query_parts: raise ValueError( "Must provide either 'query' parameter or at least one convenience parameter" ) # Combine all query parts with AND final_query = " AND ".join(query_parts) return final_query, convenience_params_used except Exception as e: raise ValueError(f"Query building failed: {str(e)}") def test_date_validation(): """Test date validation function""" print("Testing date validation...") # Valid dates assert validate_date_range("2024-01-01") == "2024-01-01" assert validate_date_range("2023-12-31") == "2023-12-31" assert validate_date_range("") is None assert validate_date_range(" ") is None assert validate_date_range(None) is None # Invalid formats try: validate_date_range("2024/01/01") assert False, "Should have failed on invalid format" except ValueError as e: assert "YYYY-MM-DD format" in str(e) try: validate_date_range("2024-13-01") assert False, "Should have failed on invalid month" except ValueError: pass # Expected # Invalid date range (too old) try: validate_date_range("1980-01-01") assert False, "Should have failed on too old date" except ValueError as e: assert "between 1990 and" in str(e) print("Date validation tests passed") def test_string_validation(): """Test string parameter validation""" print("Testing string validation...") # Valid strings assert validate_string_param("test", "Apple Inc.") == "Apple Inc." assert validate_string_param("test", " Valid Name ") == "Valid Name" assert validate_string_param("test", "") is None assert validate_string_param("test", None) is None # Invalid characters try: validate_string_param("test", "Invalid<script>") assert False, "Should have failed on invalid characters" except ValueError: pass # Expected # Too long try: validate_string_param("test", "x" * 201) assert False, "Should have failed on length" except ValueError as e: assert "too long" in str(e) print("String validation tests passed") def test_application_number_validation(): """Test application number validation and cleaning""" print("Testing application number validation...") # Valid formats assert validate_application_number("17896175") == "17896175" assert validate_application_number("17/896175") == "17896175" assert validate_application_number(" 17 896 175 ") == "17896175" assert validate_application_number("") is None assert validate_application_number(None) is None # Invalid formats try: validate_application_number("12345") # Too short assert False, "Should have failed on too short" except ValueError: pass try: validate_application_number("12345678901") # Too long assert False, "Should have failed on too long" except ValueError: pass try: validate_application_number("17ABC175") # Non-digits assert False, "Should have failed on non-digits" except ValueError: pass print("Application number validation tests passed") def test_minimal_query_building(): """Test query building for minimal tier (9 parameters only)""" print("Testing minimal query building...") # Test basic minimal parameters query, params = _build_convenience_query( applicant_name="Apple Inc.", decision_type="DENIED", patent_number="11788453", allow_balanced_params=False ) expected_parts = [ 'firstApplicantName:"Apple Inc."', 'patentNumber:11788453', 'decisionTypeCodeDescriptionText:DENIED' ] for part in expected_parts: assert part in query, f"Missing query part: {part}" assert params["applicant_name"] == "Apple Inc." assert params["decision_type"] == "DENIED" assert params["patent_number"] == "11788453" print("Minimal query building tests passed") def test_balanced_query_building(): """Test query building for balanced tier (14 parameters)""" print("Testing balanced query building...") # Test balanced parameters query, params = _build_convenience_query( applicant_name="TechCorp", decision_type="DENIED", art_unit="2128", petition_type_code="551", entity_status="Small", allow_balanced_params=True ) expected_parts = [ 'firstApplicantName:"TechCorp"', 'decisionTypeCodeDescriptionText:DENIED', 'groupArtUnitNumber:2128', 'decisionPetitionTypeCode:551', 'businessEntityStatusCategory:"Small"' ] for part in expected_parts: assert part in query, f"Missing query part: {part}" assert params["applicant_name"] == "TechCorp" assert params["art_unit"] == "2128" assert params["petition_type_code"] == "551" print("Balanced query building tests passed") def test_date_range_query_building(): """Test date range query building""" print("Testing date range query building...") # Test petition date range query, params = _build_convenience_query( petition_date_start="2024-01-01", petition_date_end="2024-12-31", allow_balanced_params=False ) assert "petitionMailDate:[2024-01-01 TO 2024-12-31]" in query assert params["petition_date_range"] == "2024-01-01 TO 2024-12-31" # Test decision date range query, params = _build_convenience_query( decision_date_start="2024-01-01", allow_balanced_params=False ) assert "decisionDate:[2024-01-01 TO *]" in query assert params["decision_date_range"] == "2024-01-01 TO *" print("Date range query building tests passed") def test_progressive_disclosure_enforcement(): """Test that minimal tier rejects balanced-only parameters""" print("Testing progressive disclosure enforcement...") # This should fail - balanced parameters in minimal tier try: _build_convenience_query( applicant_name="Apple Inc.", art_unit="2128", # Balanced only allow_balanced_params=False ) assert False, "Should have failed on balanced parameter in minimal tier" except ValueError as e: assert "art_unit" in str(e) assert "fpd_search_petitions_balanced" in str(e) # This should succeed - same parameters in balanced tier query, params = _build_convenience_query( applicant_name="Apple Inc.", art_unit="2128", allow_balanced_params=True ) assert 'firstApplicantName:"Apple Inc."' in query assert 'groupArtUnitNumber:2128' in query print("Progressive disclosure enforcement tests passed") def test_hybrid_query_and_parameters(): """Test combining query string with convenience parameters""" print("Testing hybrid query + parameters...") query, params = _build_convenience_query( query="machine learning", applicant_name="TechCorp", decision_type="DENIED", allow_balanced_params=False ) # Should contain both the base query and convenience parameters assert "(machine learning)" in query assert 'firstApplicantName:"TechCorp"' in query assert 'decisionTypeCodeDescriptionText:DENIED' in query assert params["base_query"] == "machine learning" assert params["applicant_name"] == "TechCorp" print("Hybrid query + parameters tests passed") def test_empty_parameters(): """Test behavior with no parameters provided""" print("Testing empty parameters...") # Should fail - no query or parameters try: _build_convenience_query(allow_balanced_params=False) assert False, "Should have failed with no parameters" except ValueError as e: assert "Must provide either" in str(e) print("Empty parameters tests passed") def test_application_number_cleaning(): """Test that application numbers are properly cleaned in queries""" print("Testing application number cleaning...") query, params = _build_convenience_query( application_number="17/896 175", allow_balanced_params=False ) # Should be cleaned to remove spaces and slashes assert "applicationNumberText:17896175" in query assert params["application_number"] == "17896175" print("Application number cleaning tests passed") def test_multiple_parameter_combination(): """Test combining multiple parameters correctly""" print("Testing multiple parameter combination...") query, params = _build_convenience_query( applicant_name="Apple Inc.", decision_type="DENIED", petition_date_start="2024-01-01", petition_date_end="2024-12-31", application_number="17896175", allow_balanced_params=False ) # Debug: Print the query to see what's happening print(f"Generated query: {query}") print(f"Parameters used: {params}") # Check all parts are connected with AND and_count = query.count(" AND ") # We expect 3 AND operators for 4 query parts: # 1. firstApplicantName:"Apple Inc." # 2. applicationNumberText:17896175 # 3. decisionTypeCodeDescriptionText:DENIED # 4. petitionMailDate:[2024-01-01 TO 2024-12-31] assert and_count == 3, f"Expected 3 AND operators, got {and_count}" # Check all parameters are represented (petition date start/end combine into one range) assert len(params) == 4 # 4 parameters used (date range counts as 1) print("Multiple parameter combination tests passed") def test_special_character_handling(): """Test handling of special characters in parameters""" print("Testing special character handling...") # Test quotes in company names query, params = _build_convenience_query( applicant_name="Tech Corp Inc.", allow_balanced_params=False ) # Should properly quote the company name assert 'firstApplicantName:"Tech Corp Inc."' in query # Test deciding office with quotes query, params = _build_convenience_query( deciding_office="OFFICE OF PETITIONS", allow_balanced_params=False ) assert 'finalDecidingOfficeName:"OFFICE OF PETITIONS"' in query print("Special character handling tests passed") def run_all_tests(): """Run all test functions""" print("Starting tiered convenience parameters tests...\n") try: test_date_validation() test_string_validation() test_application_number_validation() test_minimal_query_building() test_balanced_query_building() test_date_range_query_building() test_progressive_disclosure_enforcement() test_hybrid_query_and_parameters() test_empty_parameters() test_application_number_cleaning() test_multiple_parameter_combination() test_special_character_handling() print("\nAll tests passed! Tiered convenience parameters implementation is working correctly.") return True except Exception as e: print(f"\nTest failed: {e}") import traceback traceback.print_exc() return False if __name__ == "__main__": success = run_all_tests() sys.exit(0 if success else 1)

Latest Blog Posts

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/john-walkoe/uspto_fpd_mcp'

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