"""FastMCP server for Cisco Commerce Catalog API.
Provides tools for LLMs to query Cisco product pricing, details, and service mappings.
"""
import logging
from typing import Any
from fastmcp import FastMCP
from .client import CiscoCatalogClient, CiscoAPIError
from .config import Settings, get_settings
from .constants import (
AVAILABILITY_ATTRIBUTES,
BASIC_ATTRIBUTES,
EOL_ATTRIBUTES,
ITEM_ATTRIBUTES,
PHYSICAL_ATTRIBUTES,
PRICE_LISTS,
PRICING_ATTRIBUTES,
SERVICE_ATTRIBUTES,
)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# Initialize FastMCP server
mcp = FastMCP(
"Cisco Catalog",
description="Query Cisco Commerce Catalog for product pricing, details, and service mappings",
)
# Lazy-initialized client
_client: CiscoCatalogClient | None = None
def get_client() -> CiscoCatalogClient:
"""Get or create the Cisco API client."""
global _client
if _client is None:
settings = get_settings()
_client = CiscoCatalogClient(settings)
return _client
@mcp.tool()
async def lookup_pricing(
skus: list[str],
price_list: str = "GLUS",
) -> list[dict[str, Any]]:
"""
Look up Cisco product pricing for one or more SKUs.
This is the primary tool for getting product prices. Returns list price,
currency, and basic product information.
Args:
skus: List of Cisco SKUs/part numbers to look up (max 1000).
Examples: ["C9300-24T-E", "WS-C3850-24T-S", "CON-SNT-C93002TE"]
price_list: Price list code. Common options:
- GLUS: US pricing (default)
- GLEMEA: EMEA pricing
- GLEURO: Euro pricing
- GLCA: Canadian pricing
- GLGB: UK pricing in GBP
Returns:
List of pricing information for each SKU including:
- sku: The product SKU
- description: Product description
- list_price: The list price
- currency: Currency code (USD, EUR, etc.)
- product_type: Type of product (Hardware, Software, Service)
Example:
>>> await lookup_pricing(["C9300-24T-E"])
[{"sku": "C9300-24T-E", "description": "Catalyst 9300 24-port...",
"list_price": "4500.00", "currency": "USD", ...}]
"""
client = get_client()
try:
return await client.get_item_information(
skus=skus,
price_list=price_list,
attributes=PRICING_ATTRIBUTES + BASIC_ATTRIBUTES,
)
except CiscoAPIError as e:
return [{"error": f"{e.code}: {e.message}"}]
except Exception as e:
logger.exception("Error looking up pricing")
return [{"error": str(e)}]
@mcp.tool()
async def get_product_details(
sku: str,
price_list: str = "GLUS",
) -> dict[str, Any]:
"""
Get comprehensive details for a single Cisco SKU.
Returns all available attributes including pricing, availability,
physical specifications, EOL dates, licensing info, and more.
Args:
sku: Single Cisco SKU/part number to look up.
Example: "C9300-24T-E"
price_list: Price list code (default: GLUS for US pricing)
Returns:
Comprehensive product information including:
- Basic info: sku, description, product_type, erp_family
- Pricing: list_price, currency, discount info
- Availability: web_orderable, lead_time, stockable
- Physical: weight, dimensions
- EOL dates: end_of_sale, end_of_support, etc.
- Licensing: smart_licensing_enabled, license_type
- And many more attributes
Example:
>>> await get_product_details("C9300-24T-E")
{"sku": "C9300-24T-E", "description": "...", "list_price": "4500.00",
"web_orderable": "Yes", "lead_time": "14 days", ...}
"""
client = get_client()
try:
results = await client.get_item_information(
skus=[sku],
price_list=price_list,
attributes=ITEM_ATTRIBUTES, # Request ALL attributes
)
return results[0] if results else {"error": "No data returned for SKU"}
except CiscoAPIError as e:
return {"error": f"{e.code}: {e.message}"}
except Exception as e:
logger.exception("Error getting product details")
return {"error": str(e)}
@mcp.tool()
async def check_availability(
skus: list[str],
price_list: str = "GLUS",
) -> list[dict[str, Any]]:
"""
Check availability and orderability for one or more Cisco SKUs.
Use this to determine if products can be ordered, lead times,
and any fulfillment restrictions.
Args:
skus: List of Cisco SKUs to check availability for
price_list: Price list code (default: GLUS)
Returns:
List of availability information for each SKU including:
- sku: The product SKU
- web_orderable: Whether orderable online (Yes/No)
- major_line_orderable: Whether can be ordered as major line
- stockable: Whether item is stockable
- lead_time: Expected lead time
- hold_status: Current hold status
- fulfillment_restriction: Any restrictions (Yes/No/Block/No Stock)
- oea: Orderability Environment Availability
Example:
>>> await check_availability(["C9300-24T-E", "WS-C3850-24T-S"])
"""
client = get_client()
try:
return await client.get_item_information(
skus=skus,
price_list=price_list,
attributes=AVAILABILITY_ATTRIBUTES + ["ListPrice", "ProductType"],
)
except CiscoAPIError as e:
return [{"error": f"{e.code}: {e.message}"}]
except Exception as e:
logger.exception("Error checking availability")
return [{"error": str(e)}]
@mcp.tool()
async def check_end_of_life(
skus: list[str],
price_list: str = "GLUS",
) -> list[dict[str, Any]]:
"""
Check End-of-Life (EOL) dates for Cisco products.
Returns all EOL milestone dates to help with lifecycle planning
and identifying products approaching end of support.
Args:
skus: List of Cisco SKUs to check EOL status for
price_list: Price list code (default: GLUS)
Returns:
List of EOL information for each SKU including:
- sku: The product SKU
- eol_external_announce_date: When EOL was announced
- end_of_sale_date: Last date to order
- end_of_sw_availability: Last date for software
- end_of_new_service_attachment_date: Last date to attach new service
- end_of_service_contract_renewal_date: Last date to renew service
- end_of_sw_maintenance_releases: Last SW maintenance release date
- last_date_of_support: Final support date
- end_of_vulnerability_or_security_support: Security support end date
Example:
>>> await check_end_of_life(["WS-C3750-24TS-S"])
"""
client = get_client()
try:
return await client.get_item_information(
skus=skus,
price_list=price_list,
attributes=EOL_ATTRIBUTES + BASIC_ATTRIBUTES,
)
except CiscoAPIError as e:
return [{"error": f"{e.code}: {e.message}"}]
except Exception as e:
logger.exception("Error checking EOL status")
return [{"error": str(e)}]
@mcp.tool()
async def get_service_options(
skus: list[str],
price_list: str = "GLUS",
service_program: str | None = None,
) -> list[dict[str, Any]]:
"""
Get available service/support options for Cisco products.
Returns service programs, service levels, and service SKUs
that can be attached to the specified products.
Args:
skus: List of Cisco hardware/software SKUs to get service options for
price_list: Price list code (default: GLUS)
service_program: Filter by specific service program (e.g., "SNTC", "SSPT")
Use None or "All" to get all available programs
Returns:
List of service options for each SKU including:
- sku: The hardware/software SKU
- services: List of available services, each with:
- service_program: Program name (e.g., "SNTC")
- service_level: Level (e.g., "8X5XNBD")
- service_level_description: Description
- service_sku: The service part number (e.g., "CON-SNT-...")
- service_sku_description: Service description
- price: Service price (if available)
Example:
>>> await get_service_options(["C9300-24T-E"])
"""
client = get_client()
try:
return await client.get_mapped_services(
skus=skus,
price_list=price_list,
service_program=service_program,
)
except CiscoAPIError as e:
return [{"error": f"{e.code}: {e.message}"}]
except Exception as e:
logger.exception("Error getting service options")
return [{"error": str(e)}]
@mcp.tool()
async def get_physical_specs(
skus: list[str],
price_list: str = "GLUS",
) -> list[dict[str, Any]]:
"""
Get physical specifications for Cisco hardware products.
Returns dimensions, weight, and packaging information useful
for shipping and rack planning.
Args:
skus: List of Cisco hardware SKUs
price_list: Price list code (default: GLUS)
Returns:
List of physical specifications for each SKU including:
- sku: The product SKU
- description: Product description
- width: Width in inches
- length: Length in inches
- height: Height in inches
- weight: Weight in pounds
- full_carton_qty: Units per carton
- full_pallet_qty: Units per pallet
- min_unit_weight: Minimum weight (for configurable products)
- max_unit_weight: Maximum weight (for configurable products)
Example:
>>> await get_physical_specs(["C9300-24T-E"])
"""
client = get_client()
try:
return await client.get_item_information(
skus=skus,
price_list=price_list,
attributes=PHYSICAL_ATTRIBUTES + BASIC_ATTRIBUTES,
)
except CiscoAPIError as e:
return [{"error": f"{e.code}: {e.message}"}]
except Exception as e:
logger.exception("Error getting physical specs")
return [{"error": str(e)}]
@mcp.tool()
async def get_service_sku_details(
service_skus: list[str],
price_list: str = "GLUS",
) -> list[dict[str, Any]]:
"""
Get details for Cisco service/support SKUs (CON-* part numbers).
Use this to get pricing and details for service contracts.
Args:
service_skus: List of service SKUs (typically start with "CON-")
Examples: ["CON-SNT-C93002TE", "CON-SSPT-C93002TE"]
price_list: Price list code (default: GLUS)
Returns:
List of service SKU information including:
- sku: The service SKU
- description: Service description
- list_price: Service price
- service_program: Program (SNTC, SSPT, etc.)
- service_level: Level (8X5XNBD, 24X7X4, etc.)
- service_duration: Contract duration
- related_hardware_product: Associated hardware SKU
Example:
>>> await get_service_sku_details(["CON-SNT-C93002TE"])
"""
client = get_client()
try:
return await client.get_item_information(
skus=service_skus,
price_list=price_list,
attributes=SERVICE_ATTRIBUTES + PRICING_ATTRIBUTES + BASIC_ATTRIBUTES,
)
except CiscoAPIError as e:
return [{"error": f"{e.code}: {e.message}"}]
except Exception as e:
logger.exception("Error getting service SKU details")
return [{"error": str(e)}]
@mcp.tool()
async def compare_products(
skus: list[str],
price_list: str = "GLUS",
) -> dict[str, Any]:
"""
Compare multiple Cisco products side by side.
Useful for comparing pricing, features, and specifications
across similar products.
Args:
skus: List of 2-10 Cisco SKUs to compare
price_list: Price list code (default: GLUS)
Returns:
Comparison data including:
- products: List of product details for each SKU
- comparison_summary: Quick comparison of key attributes
Example:
>>> await compare_products(["C9300-24T-E", "C9300-24T-A", "C9300-48T-E"])
"""
if len(skus) < 2:
return {"error": "Please provide at least 2 SKUs to compare"}
if len(skus) > 10:
return {"error": "Maximum 10 SKUs for comparison"}
client = get_client()
try:
products = await client.get_item_information(
skus=skus,
price_list=price_list,
attributes=ITEM_ATTRIBUTES,
)
# Build comparison summary
summary = {
"price_range": {},
"product_types": set(),
"all_orderable": True,
}
prices = []
for p in products:
if "error" not in p:
price = p.get("list_price")
if price:
try:
prices.append(float(price))
except (ValueError, TypeError):
pass
if p.get("product_type"):
summary["product_types"].add(p.get("product_type"))
if p.get("web_orderable") == "No":
summary["all_orderable"] = False
if prices:
summary["price_range"] = {
"min": min(prices),
"max": max(prices),
"currency": products[0].get("currency", "USD") if products else "USD",
}
summary["product_types"] = list(summary["product_types"])
return {
"products": products,
"comparison_summary": summary,
}
except CiscoAPIError as e:
return {"error": f"{e.code}: {e.message}"}
except Exception as e:
logger.exception("Error comparing products")
return {"error": str(e)}
@mcp.tool()
def list_price_lists() -> dict[str, Any]:
"""
List all available Cisco price lists.
Returns the available price list codes and their descriptions
to help choose the right price list for your region.
Returns:
Dictionary of price lists with:
- code: Price list short code (e.g., "GLUS")
- id: ERP price list ID
- description: Full description
- currency: Currency code
Example:
>>> list_price_lists()
{"GLUS": {"id": "1109", "description": "Global Price List US...", "currency": "USD"}, ...}
"""
return PRICE_LISTS
@mcp.tool()
def list_available_attributes() -> list[str]:
"""
List all available product attributes that can be queried.
Returns the complete list of attributes available from the
Cisco Commerce Catalog API.
Returns:
List of attribute names that can be requested
Example:
>>> list_available_attributes()
["MajorLineOrderable", "Configurable", "Serialized", ...]
"""
return ITEM_ATTRIBUTES
def main() -> None:
"""Run the MCP server."""
mcp.run()
if __name__ == "__main__":
main()