"""Trade Me MCP Server - Main server implementation."""
import asyncio
import logging
import os
from typing import Any
from dotenv import load_dotenv
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .client import TradeMeClient
from .auth import TradeMeAuth
from .tools import catalogue, search, listings, bidding
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "INFO"),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# Initialize MCP server
app = Server("trademe-mcp-server")
# Initialize Trade Me client
trademe_client: TradeMeClient | None = None
def get_client() -> TradeMeClient:
"""Get or create Trade Me client instance."""
global trademe_client
if trademe_client is None:
trademe_client = TradeMeClient()
return trademe_client
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List all available MCP tools."""
return [
Tool(
name="get_categories",
description="Retrieve the full Trade Me category tree with all categories and subcategories",
inputSchema={
"type": "object",
"properties": {
"depth": {
"type": "integer",
"description": "Optional depth limit for subcategories (1-4)",
"minimum": 1,
"maximum": 4,
},
"with_counts": {
"type": "boolean",
"description": "Whether to include listing counts",
},
},
},
),
Tool(
name="get_category_details",
description="Get detailed information about a specific category including subcategories and attributes",
inputSchema={
"type": "object",
"properties": {
"category_number": {
"type": "string",
"description": "The Trade Me category number (e.g., '0004-0369-6076-')",
},
"depth": {
"type": "integer",
"description": "Optional depth limit for subcategories",
"minimum": 1,
"maximum": 4,
},
},
"required": ["category_number"],
},
),
Tool(
name="get_localities",
description="Retrieve all Trade Me localities (regions, districts, suburbs) in New Zealand",
inputSchema={
"type": "object",
"properties": {},
},
),
Tool(
name="get_locality_details",
description="Get detailed information about a specific locality",
inputSchema={
"type": "object",
"properties": {
"locality_id": {
"type": "integer",
"description": "The locality ID number",
},
},
"required": ["locality_id"],
},
),
Tool(
name="get_districts",
description="Retrieve all Trade Me districts in New Zealand",
inputSchema={
"type": "object",
"properties": {},
},
),
Tool(
name="get_attributes",
description="Get available searchable and required attributes for a specific category",
inputSchema={
"type": "object",
"properties": {
"category_number": {
"type": "string",
"description": "The Trade Me category number",
},
},
"required": ["category_number"],
},
),
Tool(
name="get_legal_notice",
description="Get the legal notice text for a category (some categories require sellers to display legal notices)",
inputSchema={
"type": "object",
"properties": {
"category_number": {
"type": "string",
"description": "The Trade Me category number",
},
},
"required": ["category_number"],
},
),
# Search tools
Tool(
name="search_general",
description="Search the Trade Me marketplace across all categories with various filtering options",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords"},
"category": {"type": "string", "description": "Category number to search within"},
"price_min": {"type": "number", "description": "Minimum price filter"},
"price_max": {"type": "number", "description": "Maximum price filter"},
"condition": {"type": "string", "description": "Item condition (New, Used, Refurbished)"},
"buy_now_only": {"type": "boolean", "description": "Only show Buy Now listings"},
"region": {"type": "integer", "description": "Region ID for location filtering"},
"district": {"type": "integer", "description": "District ID for location filtering"},
"suburb": {"type": "integer", "description": "Suburb ID for location filtering"},
"page": {"type": "integer", "description": "Page number for pagination (default: 1)"},
"rows": {"type": "integer", "description": "Results per page (default: 25, max: 500)"},
"sort_order": {"type": "string", "description": "Sort order (Default, ExpiryAsc, ExpiryDesc, PriceAsc, PriceDesc, Featured)"},
},
},
),
Tool(
name="search_property_residential",
description="Search for residential properties for sale (houses, apartments, land)",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords"},
"region": {"type": "integer", "description": "Region ID"},
"district": {"type": "integer", "description": "District ID"},
"suburb": {"type": "integer", "description": "Suburb ID"},
"price_min": {"type": "number", "description": "Minimum price"},
"price_max": {"type": "number", "description": "Maximum price"},
"bedrooms_min": {"type": "integer", "description": "Minimum bedrooms"},
"bedrooms_max": {"type": "integer", "description": "Maximum bedrooms"},
"bathrooms_min": {"type": "integer", "description": "Minimum bathrooms"},
"property_type": {"type": "string", "description": "Property type (House, Apartment, Townhouse, Unit, Land)"},
"adjacent_suburbs": {"type": "boolean", "description": "Include adjacent suburbs"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
"sort_order": {"type": "string", "description": "Sort order"},
},
},
),
Tool(
name="search_property_rental",
description="Search for rental properties (houses and apartments to rent)",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords"},
"region": {"type": "integer", "description": "Region ID"},
"district": {"type": "integer", "description": "District ID"},
"suburb": {"type": "integer", "description": "Suburb ID"},
"price_min": {"type": "number", "description": "Minimum weekly rent"},
"price_max": {"type": "number", "description": "Maximum weekly rent"},
"bedrooms_min": {"type": "integer", "description": "Minimum bedrooms"},
"bedrooms_max": {"type": "integer", "description": "Maximum bedrooms"},
"property_type": {"type": "string", "description": "Property type"},
"pets_ok": {"type": "boolean", "description": "Allow pets"},
"smokers_ok": {"type": "boolean", "description": "Allow smokers"},
"date_available": {"type": "string", "description": "Date available from (YYYY-MM-DD)"},
"adjacent_suburbs": {"type": "boolean", "description": "Include adjacent suburbs"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="search_property_commercial",
description="Search for commercial real estate (offices, retail, industrial)",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords"},
"region": {"type": "integer", "description": "Region ID"},
"district": {"type": "integer", "description": "District ID"},
"suburb": {"type": "integer", "description": "Suburb ID"},
"price_min": {"type": "number", "description": "Minimum price"},
"price_max": {"type": "number", "description": "Maximum price"},
"property_type": {"type": "string", "description": "Commercial property type"},
"area_min": {"type": "number", "description": "Minimum floor area (sqm)"},
"area_max": {"type": "number", "description": "Maximum floor area (sqm)"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="search_motors",
description="Search for motor vehicles (cars, motorcycles, boats)",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords (make/model)"},
"category": {"type": "string", "description": "Motors category"},
"price_min": {"type": "number", "description": "Minimum price"},
"price_max": {"type": "number", "description": "Maximum price"},
"year_min": {"type": "integer", "description": "Minimum year"},
"year_max": {"type": "integer", "description": "Maximum year"},
"odometer_min": {"type": "integer", "description": "Minimum odometer (km)"},
"odometer_max": {"type": "integer", "description": "Maximum odometer (km)"},
"make": {"type": "string", "description": "Vehicle make"},
"model": {"type": "string", "description": "Vehicle model"},
"body_style": {"type": "string", "description": "Body style"},
"transmission": {"type": "string", "description": "Transmission (Automatic, Manual)"},
"fuel_type": {"type": "string", "description": "Fuel type (Petrol, Diesel, Electric, Hybrid)"},
"engine_size_min": {"type": "integer", "description": "Minimum engine size (cc)"},
"engine_size_max": {"type": "integer", "description": "Maximum engine size (cc)"},
"region": {"type": "integer", "description": "Region ID"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="search_jobs",
description="Search for job listings and employment opportunities",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords (job title, company)"},
"category": {"type": "string", "description": "Jobs category number"},
"region": {"type": "integer", "description": "Region ID"},
"district": {"type": "integer", "description": "District ID"},
"type": {"type": "string", "description": "Job type (FullTime, PartTime, Contract, Casual, Temporary)"},
"pay_min": {"type": "number", "description": "Minimum salary/pay"},
"pay_max": {"type": "number", "description": "Maximum salary/pay"},
"pay_type": {"type": "string", "description": "Pay period (Annual, Hourly, Daily)"},
"work_type": {"type": "string", "description": "Work arrangement (Office, Remote, Hybrid)"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="search_services",
description="Search for professional services and tradespeople",
inputSchema={
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "Search keywords"},
"category": {"type": "string", "description": "Services category number"},
"region": {"type": "integer", "description": "Region ID"},
"district": {"type": "integer", "description": "District ID"},
"suburb": {"type": "integer", "description": "Suburb ID"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
# Listing retrieval tools
Tool(
name="get_listing_details",
description="Get comprehensive details about a specific listing including title, description, price, photos, and seller info",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
"return_metadata": {"type": "boolean", "description": "Include additional metadata"},
},
"required": ["listing_id"],
},
),
Tool(
name="get_listing_photos",
description="Get all photos for a specific listing in various sizes",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
},
"required": ["listing_id"],
},
),
Tool(
name="get_listing_questions",
description="Get questions and answers (Q&A) for a specific listing",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
"page": {"type": "integer", "description": "Page number for pagination"},
"rows": {"type": "integer", "description": "Number of results per page"},
},
"required": ["listing_id"],
},
),
Tool(
name="get_bidding_history",
description="Get the bidding history for an auction listing with bid amounts and timestamps",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
},
"required": ["listing_id"],
},
),
Tool(
name="get_shipping_options",
description="Get available shipping methods and costs for a listing",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
},
"required": ["listing_id"],
},
),
Tool(
name="get_similar_listings",
description="Find listings similar to a specified listing based on category and attributes",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
"rows": {"type": "integer", "description": "Number of results to return"},
},
"required": ["listing_id"],
},
),
Tool(
name="get_watchlist",
description="Get the authenticated user's watchlist (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"category": {"type": "string", "description": "Filter by category number"},
"page": {"type": "integer", "description": "Page number"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="add_to_watchlist",
description="Add a listing to the authenticated user's watchlist (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID to watch"},
"email_option": {"type": "integer", "description": "Email notification preference (0=None, 1=Daily, 2=Immediate)"},
},
"required": ["listing_id"],
},
),
Tool(
name="remove_from_watchlist",
description="Remove a listing from the authenticated user's watchlist (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID to unwatch"},
},
"required": ["listing_id"],
},
),
# Bidding and buying tools
Tool(
name="place_bid",
description="Place a bid on an auction listing (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID to bid on"},
"amount": {"type": "number", "description": "The bid amount in NZD"},
"shipping_option": {"type": "integer", "description": "Shipping option ID (optional)"},
},
"required": ["listing_id", "amount"],
},
),
Tool(
name="buy_now",
description="Purchase a listing using Buy Now (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID to purchase"},
"quantity": {"type": "integer", "description": "Number of items to purchase (default: 1)"},
"shipping_option": {"type": "integer", "description": "Shipping option ID"},
"accept_terms": {"type": "boolean", "description": "Accept seller's terms and conditions"},
},
"required": ["listing_id"],
},
),
Tool(
name="make_offer",
description="Make a fixed price offer on a listing (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"listing_id": {"type": "integer", "description": "The Trade Me listing ID"},
"price": {"type": "number", "description": "The offer amount in NZD"},
"message": {"type": "string", "description": "Optional message to seller"},
},
"required": ["listing_id", "price"],
},
),
Tool(
name="withdraw_offer",
description="Withdraw a previously made fixed price offer (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"offer_id": {"type": "integer", "description": "The offer ID to withdraw"},
},
"required": ["offer_id"],
},
),
Tool(
name="accept_offer",
description="Accept an offer made on your listing - seller action (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"offer_id": {"type": "integer", "description": "The offer ID to accept"},
},
"required": ["offer_id"],
},
),
Tool(
name="decline_offer",
description="Decline an offer made on your listing - seller action (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"offer_id": {"type": "integer", "description": "The offer ID to decline"},
},
"required": ["offer_id"],
},
),
Tool(
name="get_purchase_history",
description="Get the authenticated user's purchase history (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"page": {"type": "integer", "description": "Page number for pagination"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
"filter": {"type": "string", "description": "Filter by status (All, EmailSent, PaymentReceived, GoodsShipped)"},
},
},
),
Tool(
name="get_won_items",
description="Get auctions the authenticated user has won (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"page": {"type": "integer", "description": "Page number for pagination"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="get_lost_items",
description="Get auctions the authenticated user bid on but lost (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"page": {"type": "integer", "description": "Page number for pagination"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
Tool(
name="get_bidding_on",
description="Get items the authenticated user is currently bidding on (requires authentication)",
inputSchema={
"type": "object",
"properties": {
"page": {"type": "integer", "description": "Page number for pagination"},
"rows": {"type": "integer", "description": "Results per page (max 500)"},
},
},
),
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
"""Handle tool execution requests."""
client = get_client()
try:
# Route to appropriate tool function
# Catalogue tools
if name == "get_categories":
result = await catalogue.get_categories(client, **arguments)
elif name == "get_category_details":
result = await catalogue.get_category_details(client, **arguments)
elif name == "get_localities":
result = await catalogue.get_localities(client, **arguments)
elif name == "get_locality_details":
result = await catalogue.get_locality_details(client, **arguments)
elif name == "get_districts":
result = await catalogue.get_districts(client, **arguments)
elif name == "get_attributes":
result = await catalogue.get_attributes(client, **arguments)
elif name == "get_legal_notice":
result = await catalogue.get_legal_notice(client, **arguments)
# Search tools
elif name == "search_general":
result = await search.search_general(client, **arguments)
elif name == "search_property_residential":
result = await search.search_property_residential(client, **arguments)
elif name == "search_property_rental":
result = await search.search_property_rental(client, **arguments)
elif name == "search_property_commercial":
result = await search.search_property_commercial(client, **arguments)
elif name == "search_motors":
result = await search.search_motors(client, **arguments)
elif name == "search_jobs":
result = await search.search_jobs(client, **arguments)
elif name == "search_services":
result = await search.search_services(client, **arguments)
# Listing retrieval tools
elif name == "get_listing_details":
result = await listings.get_listing_details(client, **arguments)
elif name == "get_listing_photos":
result = await listings.get_listing_photos(client, **arguments)
elif name == "get_listing_questions":
result = await listings.get_listing_questions(client, **arguments)
elif name == "get_bidding_history":
result = await listings.get_bidding_history(client, **arguments)
elif name == "get_shipping_options":
result = await listings.get_shipping_options(client, **arguments)
elif name == "get_similar_listings":
result = await listings.get_similar_listings(client, **arguments)
elif name == "get_watchlist":
result = await listings.get_watchlist(client, **arguments)
elif name == "add_to_watchlist":
result = await listings.add_to_watchlist(client, **arguments)
elif name == "remove_from_watchlist":
result = await listings.remove_from_watchlist(client, **arguments)
# Bidding and buying tools
elif name == "place_bid":
result = await bidding.place_bid(client, **arguments)
elif name == "buy_now":
result = await bidding.buy_now(client, **arguments)
elif name == "make_offer":
result = await bidding.make_offer(client, **arguments)
elif name == "withdraw_offer":
result = await bidding.withdraw_offer(client, **arguments)
elif name == "accept_offer":
result = await bidding.accept_offer(client, **arguments)
elif name == "decline_offer":
result = await bidding.decline_offer(client, **arguments)
elif name == "get_purchase_history":
result = await bidding.get_purchase_history(client, **arguments)
elif name == "get_won_items":
result = await bidding.get_won_items(client, **arguments)
elif name == "get_lost_items":
result = await bidding.get_lost_items(client, **arguments)
elif name == "get_bidding_on":
result = await bidding.get_bidding_on(client, **arguments)
else:
raise ValueError(f"Unknown tool: {name}")
return [TextContent(type="text", text=str(result))]
except Exception as e:
logger.error(f"Error executing tool {name}: {str(e)}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def main() -> None:
"""Run the MCP server."""
logger.info("Starting Trade Me MCP Server")
try:
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options(),
)
finally:
# Cleanup
if trademe_client is not None:
trademe_client.close()
logger.info("Trade Me MCP Server stopped")
if __name__ == "__main__":
asyncio.run(main())