Skip to main content
Glama
models.py14.5 kB
"""Data models for Turbify Store MCP Server.""" from typing import List, Optional, Any from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator from enum import Enum from typing_extensions import Self class OrderableStatus(str, Enum): """Item orderable status.""" YES = "yes" NO = "no" class TaxableStatus(str, Enum): """Item taxable status.""" YES = "yes" NO = "no" class APIStatus(str, Enum): """API response status.""" SUCCESS = "success" ERROR = "error" # Pydantic models for FastMCP schema generation class CustomData(BaseModel): name: str = Field(description="Name of the custom attribute") value: str = Field(description="Value of the custom attribute") @model_validator(mode='after') def validate_custom_data(self) -> Self: """Validate custom data fields""" # Ensure name is not empty or just whitespace if not self.name.strip(): raise ValueError("Custom data name cannot be empty") # Value can be empty, but if provided should not be just whitespace # (empty values might be valid for some use cases) return self class ItemOption(BaseModel): name: str = Field(description="Name of the option (e.g., 'Size', 'Color')") value_list: List[str] = Field(description="List of possible values for this option") @model_validator(mode='after') def validate_option(self) -> Self: """Validate option fields""" # Ensure option name is not empty if not self.name.strip(): raise ValueError("Option name cannot be empty") # Ensure all values are non-empty and unique if not self.value_list: raise ValueError("Option must have at least one value") cleaned_values = [v.strip() for v in self.value_list if v.strip()] if len(cleaned_values) != len(self.value_list): raise ValueError("Option values cannot be empty or just whitespace") if len(cleaned_values) != len(set(cleaned_values)): raise ValueError("Option values must be unique") return self class CatalogItem(BaseModel): # Required fields id: str = Field(description="Unique item ID") name: str = Field(description="Item name") price: float = Field(description="Item price", gt=0) orderable: bool = Field(OrderableStatus.YES, description="Whether item is orderable") taxable: bool = Field(TaxableStatus.NO, description="Whether item is taxable") table_id: str = Field(description="Table ID where item belongs") # Optional pricing fields sale_price: Optional[float] = Field(None, description="Sale price of the item") ship_weight: Optional[float] = Field(None, description="Shipping weight of the item") # Optional descriptive fields headline: Optional[str] = Field(None, description="Item headline (used instead of name on item page)") caption: Optional[str] = Field(None, description="Item description/caption") abstract: Optional[str] = Field(None, description="Text used for description on other pages") label: Optional[str] = Field(None, description="Text used when item appears as special on home page") # Optional product information manufacturer: Optional[str] = Field(None, description="Item manufacturer") brand: Optional[str] = Field(None, description="Item brand") gender: Optional[str] = Field(None, description="Gender (men/women/unisex) - for apparel") color: Optional[str] = Field(None, description="Color of the item") size: Optional[str] = Field(None, description="Size of the item") # Optional product codes and identifiers upc: Optional[str] = Field(None, description="Universal Product Code (12-digit)") manufacturer_part_number: Optional[str] = Field(None, description="Manufacturer's part number") model_number: Optional[str] = Field(None, description="Model number") isbn: Optional[str] = Field(None, description="International Standard Book Number") ean: Optional[str] = Field(None, description="European Article Number (13-digit)") # Optional categorization classification: Optional[str] = Field(None, description="Item classification", pattern="^(new|overstock|damaged|returned|refurbished|open box|liquidation|used)$") condition: Optional[str] = Field(None, description="Item condition", pattern="^(New|Like new|Very good|Good|Acceptable|Refurbished|Used)$") merchant_category: Optional[str] = Field(None, description="Merchant category for Yahoo Shopping") # Optional availability and shipping availability: Optional[str] = Field(None, description="Availability status", pattern="^(SAME_DAY|NEXT_DAY|2_3_DAY|3_4_DAY|5_7_DAY|1_2_WEEKS|2_3_WEEKS|4_6_WEEKS|6_8_WEEKS|CONTACT_US|IN_STOCK|AVAILABLE|OUT_OF_STOCK|PRE_ORDER)$") # Optional checkout requirements need_bill: Optional[bool] = Field(None, description="Whether billing address is required") need_payment: Optional[bool] = Field(None, description="Whether payment information is required") need_pay_ship: Optional[bool] = Field(None, description="Whether shipping address is required") # Optional pricing and promotions personalization_charge: Optional[float] = Field(None, description="Charge for monogram/inscription") msrp: Optional[str] = Field(None, description="Manufacturer's suggested retail price") # Optional web and shopping integration product_url: Optional[str] = Field(None, description="Product URL") inyshopping: Optional[bool] = Field(None, description="Include in Yahoo Shopping") yahoo_shopping_category: Optional[str] = Field(None, description="Yahoo Shopping category") # Optional age and media information age_group: Optional[str] = Field(None, description="Age group", pattern="^(infant|toddler|child|pre-teen|teen|adult)$") age_range: Optional[str] = Field(None, description="Age range (for toys)") medium: Optional[str] = Field(None, description="Media type for music/video", pattern="^(CD|Casette|MiniDisc|LPd|EP|45|VHS|Beta|8mm|Laser Disc|DVD|VCD)$") # Optional style information style_number: Optional[str] = Field(None, description="Style number (for apparel/home & garden)") style: Optional[str] = Field(None, description="Style description (e.g., 'chino', 'denim')") promo_text: Optional[str] = Field(None, description="Promotional text (up to 50 chars)", max_length=50) # Optional flags gift_cert: Optional[bool] = Field(None, description="Whether item is a gift certificate") # Custom data and options custom_data: List[CustomData] = Field(default=[], description="List of custom attributes") options: List[ItemOption] = Field(default=[], description="List of item options (size, color, etc.)") # @model_validator(mode='after') # def validate_catalog_item(self) -> Self: # """Validate catalog item according to Yahoo API requirements""" # # Validate sale price is not higher than regular price # if self.sale_price is not None and self.sale_price > self.price: # raise ValueError("Sale price cannot be higher than regular price") # # Validate UPC format (12 digits) # if self.upc is not None and not (self.upc.isdigit() and len(self.upc) == 12): # raise ValueError("UPC must be exactly 12 digits") # # Validate EAN format (13 digits) # if self.ean is not None and not (self.ean.isdigit() and len(self.ean) == 13): # raise ValueError("EAN must be exactly 13 digits") # # Validate ISBN format (10 or 13 digits) # if self.isbn is not None: # isbn_clean = self.isbn.replace('-', '').replace(' ', '') # if not (isbn_clean.isdigit() and len(isbn_clean) in [10, 13]): # raise ValueError("ISBN must be 10 or 13 digits (hyphens/spaces allowed)") # # Validate weight is positive # if self.ship_weight is not None and self.ship_weight <= 0: # raise ValueError("Ship weight must be positive") # # Validate personalization charge is not negative # if self.personalization_charge is not None and self.personalization_charge < 0: # raise ValueError("Personalization charge cannot be negative") # # Validate URL format # if self.product_url is not None: # if not (self.product_url.startswith('http://') or self.product_url.startswith('https://')): # raise ValueError("Product URL must start with http:// or https://") # # Validate custom data doesn't exceed limit (100 per API spec) # if len(self.custom_data) > 100: # raise ValueError("Cannot have more than 100 custom data attributes") # # Validate custom data names are unique # custom_names = [cd.name for cd in self.custom_data] # if len(custom_names) != len(set(custom_names)): # raise ValueError("Custom data attribute names must be unique") # # Validate option names are unique # option_names = [opt.name for opt in self.options] # if len(option_names) != len(set(option_names)): # raise ValueError("Option names must be unique") # # Validate each option has at least one value # for option in self.options: # if not option.value_list: # raise ValueError(f"Option '{option.name}' must have at least one value") # # Business logic validations # if not self.orderable and self.price > 0: # # Warning: might want to log this rather than raise # pass # Non-orderable items with prices might be valid for display # # Validate required fields for specific classifications # if self.classification == "used" and self.condition is None: # raise ValueError("Condition is required when classification is 'used'") # # Validate gender field for apparel # if self.gender is not None and self.gender not in ["men", "women", "unisex"]: # raise ValueError("Gender must be 'men', 'women', or 'unisex'") class SearchQuery(BaseModel): """Model for catalog search queries.""" keyword: str = Field(..., min_length=1, description="Search keyword") start_index: int = Field(1, ge=1, description="Start index for pagination") end_index: int = Field(100, ge=1, le=1000, description="End index for pagination") @model_validator(mode='after') def validate_indices(self) -> 'SearchQuery': """Ensure end_index is greater than start_index.""" if self.end_index <= self.start_index: raise ValueError('end_index must be greater than start_index') return self # Response Models class APIError(BaseModel): """API error information.""" code: str = Field(..., description="Error code") message: str = Field(..., description="Error message") class APIMessage(BaseModel): """API success message information.""" code: str = Field(..., description="Message code") message: str = Field(..., description="Message text") class Item(BaseModel): """Catalog item information.""" id: Optional[str] = Field(None, description="Item ID", alias="ID") name: Optional[str] = Field(None, description="Item name", alias="Name") code: Optional[str] = Field(None, description="Item code", alias="Code") price: Optional[float] = Field(None, description="Item price", alias="Price") sale_price: Optional[float] = Field(None, description="Sale price", alias="SalePrice") ship_weight: Optional[float] = Field(None, description="Shipping weight", alias="ShipWeight") orderable: Optional[bool] = Field(None, description="Orderable status", alias="Orderable") taxable: Optional[bool] = Field(None, description="Taxable status", alias="Taxable") # Fixed: Changed parameter name for Pydantic v2 model_config = ConfigDict(extra="allow", populate_by_name=True) @field_validator('price', 'sale_price', 'ship_weight', mode='before') @classmethod def convert_numeric_fields(cls, value: Any) -> Optional[float]: """Convert string values to floats, handling empty strings and None""" if value in (None, ""): return None try: return float(value) except (TypeError, ValueError): return None class APIResponse(BaseModel): """Base API response model.""" status: APIStatus = Field(..., description="Response status") errors: Optional[List[APIError]] = Field(None, description="List of errors if any") messages: Optional[List[APIMessage]] = Field(None, description="List of success messages") items: Optional[List[Item]] = Field(None, description="List of items (for search/get operations)") item_ids: Optional[List[str]] = Field(None, description="List of item IDs (for some operations)") @property def is_success(self) -> bool: """Check if the response indicates success.""" return self.status == APIStatus.SUCCESS @property def error_messages(self) -> List[str]: """Get list of error messages.""" if not self.errors: return [] return [error.message for error in self.errors] @property def success_messages(self) -> List[str]: """Get list of success messages.""" if not self.messages: return [] return [message.message for message in self.messages] class StoreConfig(BaseModel): """Store configuration information.""" store_id: str = Field(..., description="Turbify Store ID") api_base: str = Field(..., description="API base URL") max_items_per_call: int = Field(..., description="Maximum items per API call") contract_token_configured: bool = Field(..., description="Whether contract token is configured") api_version: str = Field(..., description="API version") request_timeout: int = Field(..., description="Request timeout in seconds") # Utility types XMLElement = Any # xml.etree.ElementTree.Element, but avoiding import here

Implementation Reference

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/benpeke/turbify_store_mcp'

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