Skip to main content
Glama
minted_mcp_server.py27.4 kB
#!/usr/bin/env python3 """ MCP Server for Minted.com API Interactions Provides tools for accessing Minted.com address book, orders, and delivery information. Uses the same authentication pattern as the original minted-export scripts. Based on: https://github.com/wkarney/minted-export """ import json import os import sys from pathlib import Path from time import sleep from typing import Any, Dict, List, Optional, Tuple import requests from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from selenium.webdriver import Chrome from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from webdriver_manager.chrome import ChromeDriverManager # Project root directory PROJECT_ROOT = Path(__file__).parent.parent.parent # Try to import credential utility try: sys.path.insert(0, str(PROJECT_ROOT)) from scripts.credentials import get_credential_by_domain HAS_CREDENTIALS_MODULE = True except ImportError: HAS_CREDENTIALS_MODULE = False # Initialize MCP server app = Server("minted") # Global session cache _session_cache: Dict[str, Any] = {} def get_minted_credentials() -> Tuple[str, str]: """Get Minted credentials from various sources.""" # Try 1Password first if HAS_CREDENTIALS_MODULE: try: email, password = get_credential_by_domain("minted.com") return email, password except Exception: pass # Fall back to environment variables email = os.environ.get("minted_email") password = os.environ.get("minted_password") if email and password: return email, password raise ValueError( "Minted credentials not found. Set minted_email and minted_password " "environment variables or configure 1Password credentials." ) def get_authenticated_session() -> Dict[str, str]: """Get authenticated session cookies for Minted API.""" cache_key = "minted_cookies" # Check cache (could be extended to persist across calls) if cache_key in _session_cache: return _session_cache[cache_key] email, password = get_minted_credentials() # Webdriver options options = Options() options.add_argument("--headless") service = Service(ChromeDriverManager().install()) driver = Chrome(service=service, options=options) try: URL = "https://www.minted.com/login" driver.get(URL) # Selenium handles login form email_elem = driver.find_element(By.XPATH, '//*[@id="identifierMNTD"]') email_elem.send_keys(email) password_elem = driver.find_element(By.XPATH, '//*[@id="password"]') password_elem.send_keys(password) login_submit = driver.find_element( By.XPATH, '//*[@id="__next"]/div[3]/div/form/div[2]/div[1]/button' ) login_submit.click() sleep(5) # Wait for login to complete # Obtain cookies from selenium session cookies = {c["name"]: c["value"] for c in driver.get_cookies()} # Cache cookies _session_cache[cache_key] = cookies return cookies finally: driver.close() @app.list_tools() async def list_tools() -> List[Tool]: """List available Minted API tools.""" return [ Tool( name="get_minted_contacts", description="Get all contacts from Minted address book", inputSchema={ "type": "object", "properties": {}, }, ), Tool( name="get_minted_latest_delivery", description="Get recipients from the latest Minted card delivery/order", inputSchema={ "type": "object", "properties": {}, }, ), Tool( name="get_minted_orders", description="Get order history from Minted", inputSchema={ "type": "object", "properties": { "limit": { "type": "integer", "description": "Maximum number of orders to return (default: 10)", "default": 10, }, }, }, ), ] @app.call_tool() async def call_tool(name: str, arguments: Any) -> List[TextContent]: """Handle tool calls.""" if name == "get_minted_contacts": try: cookies = get_authenticated_session() # Request address book contents as json response = requests.get( "https://addressbook.minted.com/api/contacts/contacts/?format=json", cookies=cookies, timeout=300, ) response.raise_for_status() contacts = response.json() return [TextContent( type="text", text=json.dumps({ "success": True, "count": len(contacts), "contacts": contacts, }, indent=2, default=str) )] except Exception as e: import traceback return [TextContent( type="text", text=json.dumps({ "error": str(e), "traceback": traceback.format_exc(), }, indent=2) )] elif name == "get_minted_latest_delivery": try: # First try API endpoints cookies = get_authenticated_session() # Try the groups endpoint first (discovered via diagnostic) # This endpoint returns groups with type "completeorder" containing recipient contact IDs delivery_data = None working_endpoint = None try: groups_response = requests.get( "https://addressbook.minted.com/api/contacts/groups/", cookies=cookies, timeout=30, ) if groups_response.status_code == 200: groups_data = groups_response.json() if isinstance(groups_data, list) and len(groups_data) > 0: # Filter for completeorder type groups (these are card deliveries) order_groups = [g for g in groups_data if g.get('type') == 'completeorder'] if order_groups: # Get the most recent order (first in list, or sort by created_at) latest_group = order_groups[0] if len(order_groups) > 1: try: latest_group = max(order_groups, key=lambda x: x.get('created_at', '')) except: latest_group = order_groups[0] # Get contact details for recipients contact_ids = latest_group.get('contacts', []) individuals = latest_group.get('individuals', []) # If we have individuals, use those; otherwise fetch contacts by ID if individuals and len(individuals) > 0: recipients = individuals elif contact_ids: # Fetch contact details contacts_response = requests.get( "https://addressbook.minted.com/api/contacts/contacts/?format=json", cookies=cookies, timeout=30, ) if contacts_response.status_code == 200: all_contacts = contacts_response.json() # Filter contacts by IDs recipients = [c for c in all_contacts if c.get('id') in contact_ids] else: recipients = [] else: recipients = [] # Build delivery data structure delivery_data = { "id": latest_group.get('id'), "title": latest_group.get('title'), "created_at": latest_group.get('created_at'), "recipients": recipients, "recipient_count": len(recipients), "contact_ids": contact_ids, } working_endpoint = "https://addressbook.minted.com/api/contacts/groups/" except Exception: pass # Fallback: Try other endpoints if groups endpoint didn't work if not delivery_data: endpoints_to_try = [ ("https://www.minted.com/order/list-by-uid/", {}), ("https://www.minted.com/order/list-by-uid/?format=json", {}), ("https://addressbook.minted.com/api/orders/", {}), ("https://addressbook.minted.com/api/orders/?format=json", {}), ("https://addressbook.minted.com/api/deliveries/", {}), ("https://addressbook.minted.com/api/shipments/", {}), ("https://www.minted.com/api/orders/", {}), ("https://www.minted.com/api/orders/?format=json", {}), ("https://www.minted.com/api/deliveries/", {}), ("https://www.minted.com/api/shipments/", {}), ("https://addressbook.minted.com/api/addressbook/orders/", {}), ("https://addressbook.minted.com/api/addressbook/deliveries/", {}), ] for endpoint, params in endpoints_to_try: try: response = requests.get( endpoint, cookies=cookies, params=params, timeout=30, ) if response.status_code == 200: data = response.json() # Check if this looks like delivery/order data if isinstance(data, (list, dict)) and len(data) > 0: working_endpoint = endpoint delivery_data = data break except Exception: continue # If no direct endpoint works, try getting orders list and finding the latest if not delivery_data: # Try to get orders list first (using discovered endpoint) orders_endpoints = [ "https://www.minted.com/order/list-by-uid/", "https://www.minted.com/order/list-by-uid/?format=json", "https://addressbook.minted.com/api/orders/?format=json", "https://www.minted.com/api/orders/?format=json", "https://addressbook.minted.com/api/orders/", ] for endpoint in orders_endpoints: try: response = requests.get( endpoint, cookies=cookies, timeout=30, ) if response.status_code == 200: data = response.json() if isinstance(data, list) and len(data) > 0: # Get the first (most recent) order latest_order = data[0] # Try to get full order details order_id = latest_order.get('id') or latest_order.get('order_id') or latest_order.get('order_number') or latest_order.get('orderId') if order_id: detail_endpoints = [ f"https://www.minted.com/order/detail/status/{order_id}", f"https://addressbook.minted.com/api/orders/{order_id}/?format=json", f"https://www.minted.com/api/orders/{order_id}/?format=json", f"https://addressbook.minted.com/api/orders/{order_id}/", ] for detail_endpoint in detail_endpoints: try: detail_response = requests.get( detail_endpoint, cookies=cookies, timeout=30, ) if detail_response.status_code == 200: delivery_data = detail_response.json() working_endpoint = detail_endpoint break except: continue if delivery_data: break # If detail fetch failed, use the order summary delivery_data = latest_order working_endpoint = endpoint break except Exception: continue # If API endpoints don't work, try scraping the finalize page if not delivery_data: try: # Use Selenium to navigate to finalize page and extract data email, password = get_minted_credentials() options = Options() options.add_argument("--headless") service = Service(ChromeDriverManager().install()) driver = Chrome(service=service, options=options) try: URL = "https://www.minted.com/login" driver.get(URL) email_elem = driver.find_element(By.XPATH, '//*[@id="identifierMNTD"]') email_elem.send_keys(email) password_elem = driver.find_element(By.XPATH, '//*[@id="password"]') password_elem.send_keys(password) login_submit = driver.find_element( By.XPATH, '//*[@id="__next"]/div[3]/div/form/div[2]/div[1]/button' ) login_submit.click() sleep(5) # Try to navigate to finalize page finalize_urls = [ "https://www.minted.com/addressbook/my-account/finalize/0", "https://addressbook.minted.com/my-account/finalize/0", "https://www.minted.com/addressbook/finalize", ] for finalize_url in finalize_urls: try: driver.get(finalize_url) sleep(3) # Try to extract recipient data from page page_text = driver.page_source # Try to find JSON data embedded in the page import re json_match = re.search(r'window\.__INITIAL_STATE__\s*=\s*({.+?});', page_text, re.DOTALL) if json_match: try: page_data = json.loads(json_match.group(1)) # Look for order/delivery data in the page state if 'orders' in page_data or 'deliveries' in page_data or 'finalize' in page_data: delivery_data = page_data working_endpoint = f"scraped_from:{finalize_url}" break except: pass # Try to find recipient elements on the page try: recipient_elements = driver.find_elements(By.CSS_SELECTOR, "[data-recipient], .recipient, [class*='recipient'], [class*='address']") if recipient_elements: recipients_list = [] for elem in recipient_elements: try: text = elem.text if text and len(text) > 10: # Likely an address recipients_list.append({"address": text}) except: pass if recipients_list: delivery_data = { "recipients": recipients_list, "source": "scraped_from_page" } working_endpoint = f"scraped_from:{finalize_url}" break except: pass except Exception: continue finally: driver.close() except Exception as scrape_error: pass # Fall through to error return if not delivery_data: endpoint_list = [e[0] if isinstance(e, tuple) else e for e in endpoints_to_try] return [TextContent( type="text", text=json.dumps({ "error": "Could not retrieve delivery information. No working API endpoint found and page scraping failed.", "tried_endpoints": endpoint_list, "suggestion": "Try accessing https://www.minted.com/addressbook/my-account/finalize/0 manually to view your latest delivery", }, indent=2) )] # Handle different response formats if isinstance(delivery_data, list): # Find the most recent by date latest = delivery_data[0] if len(delivery_data) > 1: try: latest = max(delivery_data, key=lambda x: x.get('created_at', x.get('date', x.get('order_date', '')))) except: latest = delivery_data[0] elif isinstance(delivery_data, dict): latest = delivery_data else: latest = {"raw_data": delivery_data} # Extract recipients - if delivery_data already has recipients (from groups endpoint), use them recipients = [] if 'recipients' in latest and latest['recipients']: recipients = latest['recipients'] if isinstance(latest['recipients'], list) else [latest['recipients']] # Otherwise try other field names elif 'addresses' in latest: recipients = latest['addresses'] if isinstance(latest['addresses'], list) else [latest['addresses']] elif 'contacts' in latest: recipients = latest['contacts'] if isinstance(latest['contacts'], list) else [latest['contacts']] elif 'recipient_addresses' in latest: recipients = latest['recipient_addresses'] if isinstance(latest['recipient_addresses'], list) else [latest['recipient_addresses']] elif 'shipping_addresses' in latest: recipients = latest['shipping_addresses'] if isinstance(latest['shipping_addresses'], list) else [latest['shipping_addresses']] # Items might contain recipient info elif 'items' in latest: for item in latest['items']: if 'recipient' in item: recipients.append(item['recipient']) elif 'address' in item: recipients.append(item['address']) elif 'recipient_address' in item: recipients.append(item['recipient_address']) elif 'shipping_address' in item: recipients.append(item['shipping_address']) # Check if the order itself has address fields (single recipient order) elif any(field in latest for field in ['name', 'address1', 'address', 'recipient_name', 'shipping_name']): recipients = [latest] # If no recipients found, return the raw data structure for inspection if not recipients and isinstance(latest, dict): # Try to find any nested structures that might contain addresses for key, value in latest.items(): if isinstance(value, list) and len(value) > 0: # Check if list items look like addresses/recipients if isinstance(value[0], dict) and any(addr_field in value[0] for addr_field in ['name', 'address1', 'address', 'city', 'zip']): recipients = value break return [TextContent( type="text", text=json.dumps({ "success": True, "endpoint": working_endpoint, "delivery_date": latest.get('created_at', latest.get('date', latest.get('order_date', 'Unknown'))), "order_id": latest.get('id', latest.get('order_id', latest.get('order_number', 'Unknown'))), "status": latest.get('status', 'Unknown'), "recipient_count": len(recipients), "recipients": recipients, "raw_delivery_data": latest, }, indent=2, default=str) )] except Exception as e: import traceback return [TextContent( type="text", text=json.dumps({ "error": str(e), "traceback": traceback.format_exc(), }, indent=2) )] elif name == "get_minted_orders": try: limit = arguments.get("limit", 10) cookies = get_authenticated_session() # Try various API endpoints for orders endpoints_to_try = [ "https://addressbook.minted.com/api/orders/", "https://www.minted.com/api/orders/", "https://addressbook.minted.com/api/addressbook/orders/", ] orders_data = None working_endpoint = None for endpoint in endpoints_to_try: try: response = requests.get( endpoint, cookies=cookies, timeout=30, ) if response.status_code == 200: data = response.json() if isinstance(data, (list, dict)) and len(data) > 0: working_endpoint = endpoint orders_data = data break except Exception: continue if not orders_data: return [TextContent( type="text", text=json.dumps({ "error": "Could not retrieve orders. No working API endpoint found.", "tried_endpoints": endpoints_to_try, }, indent=2) )] # Handle different response formats if isinstance(orders_data, list): # Sort by date (most recent first) and limit try: orders_data = sorted( orders_data, key=lambda x: x.get('created_at', x.get('date', x.get('order_date', ''))), reverse=True )[:limit] except: orders_data = orders_data[:limit] elif isinstance(orders_data, dict): orders_data = [orders_data] return [TextContent( type="text", text=json.dumps({ "success": True, "endpoint": working_endpoint, "count": len(orders_data), "orders": orders_data, }, indent=2, default=str) )] except Exception as e: import traceback return [TextContent( type="text", text=json.dumps({ "error": str(e), "traceback": traceback.format_exc(), }, indent=2) )] else: return [TextContent( type="text", text=json.dumps({ "error": f"Unknown tool: {name}", }, indent=2) )] async def main(): """Run the MCP server.""" async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() ) if __name__ == "__main__": import asyncio try: asyncio.run(main()) except KeyboardInterrupt: pass except Exception as e: import traceback print(json.dumps({ "error": str(e), "traceback": traceback.format_exc() }), file=sys.stderr) sys.exit(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/markmhendrickson/mcp-server-minted'

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