Skip to main content
Glama

Extend AI Toolkit MCP Server

Official
functions.py21.5 kB
import io import logging import os import uuid from datetime import datetime, timedelta from typing import Dict, Optional, List from extend import ExtendClient logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) pending_selections = {} # ========================= # Virtual Card Functions # ========================= async def get_virtual_cards( extend: ExtendClient, page: int = 0, per_page: int = 10, status: Optional[str] = None, recipient: Optional[str] = None, search_term: Optional[str] = None, sort_field: Optional[str] = None, sort_direction: Optional[str] = None, ) -> Dict: """Get list of virtual cards Args: page (int): The page number for pagination. Defaults to 0. per_page (int): The number of virtual cards to return per page. Defaults to 10. status (Optional[str]): Filter cards by status (e.g., "ACTIVE", "CANCELLED", "PENDING", "EXPIRED", "CLOSED", "CONSUMED") recipient (Optional[str], optional): A filter by recipient identifier. Defaults to None. search_term (Optional[str], optional): A term to search virtual cards by. Defaults to None. sort_field (Optional[str]): Field to sort by "createdAt", "updatedAt", "balanceCents", "displayName", "type", or "status" sort_direction (Optional[str]): Direction to sort (ASC or DESC) """ try: response = await extend.virtual_cards.get_virtual_cards( page=page, per_page=per_page, status=status.upper() if status else None, recipient=recipient, search_term=search_term, sort_field=sort_field, sort_direction=sort_direction ) return response except Exception as e: logger.error("Error getting virtual cards: %s", e) raise Exception("Error getting virtual cards: %s", e) async def get_virtual_card_detail(extend: ExtendClient, virtual_card_id: str) -> Dict: """Get details of a specific virtual card""" try: response = await extend.virtual_cards.get_virtual_card_detail(virtual_card_id) return response except Exception as e: logger.error("Error getting virtual card detail: %s", e) raise Exception(e) async def close_virtual_card(extend: ExtendClient, virtual_card_id: str) -> Dict: """Close a specific virtual card""" try: response = await extend.virtual_cards.close_virtual_card(virtual_card_id) return response except Exception as e: logger.error("Error closing virtual card: %s", e) raise Exception("Error closing virtual card") async def cancel_virtual_card(extend: ExtendClient, virtual_card_id: str) -> Dict: """Cancel a specific virtual card""" try: response = await extend.virtual_cards.cancel_virtual_card(virtual_card_id) return response except Exception as e: logger.error("Error canceling virtual card: %s", e) raise Exception("Error canceling virtual card") # ========================= # Transaction Functions # ========================= async def get_transactions( extend: ExtendClient, page: int = 0, per_page: int = 50, from_date: Optional[str] = None, to_date: Optional[str] = None, status: Optional[str] = None, virtual_card_id: Optional[str] = None, min_amount_cents: Optional[int] = None, max_amount_cents: Optional[int] = None, receipt_missing: Optional[bool] = None, search_term: Optional[str] = None, sort_field: Optional[str] = None, ) -> Dict: """ Get a list of recent transactions Args: page (int): pagination page number, per_page (int): number of transactions per page, from_date (Optional[str]): Start date (YYYY-MM-DD) to_date (Optional[str]): End date (YYYY-MM-DD) status (Optional[str]): Filter transactions by status (e.g., "PENDING", "CLEARED", "DECLINED", "NO_MATCH", "AVS_PASS", "AVS_FAIL", "AUTH_REVERSAL") virtual_card_id (Optional[str]): Filter by specific virtual card min_amount_cents (Optional[int]): Minimum amount in cents max_amount_cents (Optional[int]): Maximum amount in cents receipt_missing (Optional[bool]): Filter transactions by whether they are missing a receipt search_term (Optional[str]): Filter transactions by search term (e.g., "Subscription") sort_field (Optional[str]): Field to sort by, with optional direction Use "recipientName", "merchantName", "amount", "date" for ASC Use "-recipientName", "-merchantName", "-amount", "-date" for DESC """ try: response = await extend.transactions.get_transactions( page=page, per_page=per_page, from_date=from_date, to_date=to_date, status=status.upper() if status else None, virtual_card_id=virtual_card_id, min_amount_cents=min_amount_cents, max_amount_cents=max_amount_cents, search_term=search_term, sort_field=sort_field, receipt_missing=receipt_missing, ) return response except Exception as e: logger.error("Error getting transactions: %s", e) raise Exception("Error getting transactions") async def get_transaction_detail(extend: ExtendClient, transaction_id: str) -> Dict: """Get a transaction detail""" try: response = await extend.transactions.get_transaction(transaction_id) return response except Exception as e: logger.error("Error getting transaction detail: %s", e) raise Exception("Error getting transaction detail") # ========================= # Credit Card Functions # ========================= async def get_credit_cards( extend: ExtendClient, page: int = 0, per_page: int = 10, status: Optional[str] = None, search_term: Optional[str] = None, sort_direction: Optional[str] = None, ) -> Dict: """Get a list of credit cards""" try: response = await extend.credit_cards.get_credit_cards( page=page, per_page=per_page, status=status.upper() if status else None, search_term=search_term, sort_direction=sort_direction, ) return response except Exception as e: logger.error("Error getting credit cards: %s", e) raise Exception("Error getting credit cards") async def get_credit_card_detail(extend: ExtendClient, credit_card_id: str) -> Dict: """Get details of a specific credit card""" try: response = await extend.virtual_cards.get_credit_card_detail(credit_card_id) return response except Exception as e: logger.error("Error getting credit card details: %s", e) raise Exception(e) # ========================= # Expense Data Functions # ========================= async def get_expense_categories( extend: ExtendClient, active: Optional[bool] = None, required: Optional[bool] = None, search: Optional[str] = None, sort_field: Optional[str] = None, sort_direction: Optional[str] = None, ) -> Dict: """ Get a list of expense categories. """ try: response = await extend.expense_data.get_expense_categories( active=active, required=required, search=search, sort_field=sort_field, sort_direction=sort_direction, ) return response except Exception as e: logger.error("Error getting expense categories: %s", e) raise Exception("Error getting expense categories: %s", e) async def get_expense_category(extend: ExtendClient, category_id: str) -> Dict: """ Get detailed information about a specific expense category. """ try: response = await extend.expense_data.get_expense_category(category_id) return response except Exception as e: logger.error("Error getting expense category: %s", e) raise Exception("Error getting expense category: %s", e) async def get_expense_category_labels( extend: ExtendClient, category_id: str, page: Optional[int] = None, per_page: Optional[int] = None, active: Optional[bool] = None, search: Optional[str] = None, sort_field: Optional[str] = None, sort_direction: Optional[str] = None, ) -> Dict: """ Get a paginated list of expense category labels. """ try: response = await extend.expense_data.get_expense_category_labels( category_id=category_id, page=page, per_page=per_page, active=active, search=search, sort_field=sort_field, sort_direction=sort_direction, ) return response except Exception as e: logger.error("Error getting expense category labels: %s", e) raise Exception("Error getting expense category labels: %s", e) async def create_expense_category( extend: ExtendClient, name: str, code: str, required: bool, active: Optional[bool] = None, free_text_allowed: Optional[bool] = None, ) -> Dict: """ Create an expense category. """ try: response = await extend.expense_data.create_expense_category( name=name, code=code, required=required, active=active, free_text_allowed=free_text_allowed, ) return response except Exception as e: logger.error("Error creating expense category: %s", e) raise Exception("Error creating expense category: %s", e) async def create_expense_category_label( extend: ExtendClient, category_id: str, name: str, code: str, active: bool = True ) -> Dict: """ Create an expense category label. """ try: response = await extend.expense_data.create_expense_category_label( category_id=category_id, name=name, code=code, active=active ) return response except Exception as e: logger.error("Error creating expense category label: %s", e) raise Exception("Error creating expense category label: %s", e) async def update_expense_category( extend: ExtendClient, category_id: str, name: Optional[str] = None, active: Optional[bool] = None, required: Optional[bool] = None, free_text_allowed: Optional[bool] = None, ) -> Dict: """ Update an expense category. """ try: response = await extend.expense_data.update_expense_category( category_id=category_id, name=name, active=active, required=required, free_text_allowed=free_text_allowed, ) return response except Exception as e: logger.error("Error updating expense category: %s", e) raise Exception("Error updating expense category: %s", e) async def update_expense_category_label( extend: ExtendClient, category_id: str, label_id: str, name: Optional[str] = None, active: Optional[bool] = None, ) -> Dict: """ Update an expense category label. """ try: response = await extend.expense_data.update_expense_category_label( category_id=category_id, label_id=label_id, name=name, active=active, ) return response except Exception as e: logger.error("Error updating expense category label: %s", e) raise Exception("Error updating expense category label: %s", e) async def propose_transaction_expense_data( extend: ExtendClient, transaction_id: str, data: Dict ) -> Dict: """ Propose expense data changes for a transaction without applying them. Args: extend: The Extend client instance transaction_id: The unique identifier of the transaction data: A dictionary representing the expense data to update Returns: Dict: A confirmation request with token and expiration """ # Fetch transaction to ensure it exists transaction = await extend.transactions.get_transaction(transaction_id) # Generate a unique confirmation token confirmation_token = str(uuid.uuid4()) # Set expiration time (10 minutes from now) expiration_time = datetime.now() + timedelta(minutes=10) # Store the pending selection with its metadata pending_selections[confirmation_token] = { "transaction_id": transaction_id, "data": data, "created_at": datetime.now().isoformat(), "expires_at": expiration_time.isoformat(), "status": "pending" } # Return the confirmation request return { "status": "pending_confirmation", "transaction_id": transaction_id, "confirmation_token": confirmation_token, "expires_at": expiration_time.isoformat(), "proposed_categories": [ {"categoryId": category.get("categoryId", "Unknown"), "labelId": category.get("labelId", "None")} for category in data.get("expenseDetails", []) ] } async def confirm_transaction_expense_data( extend: ExtendClient, confirmation_token: str ) -> Dict: """ Confirm and apply previously proposed expense data changes. Args: extend: The Extend client instance confirmation_token: The unique token from the proposal step Returns: Dict: The updated transaction details """ # Check if token exists if confirmation_token not in pending_selections: raise Exception("Invalid confirmation token") # Get the pending selection selection = pending_selections[confirmation_token] # Check if expired if datetime.now() > datetime.fromisoformat(selection["expires_at"]): # Clean up expired token del pending_selections[confirmation_token] raise Exception("Confirmation token has expired") # Apply the expense data update response = await extend.transactions.update_transaction_expense_data( selection["transaction_id"], selection["data"] ) # Mark as confirmed and clean up selection["status"] = "confirmed" selection["confirmed_at"] = datetime.now().isoformat() # In a real implementation, you might want to keep the record for auditing # but for simplicity, we'll delete it here del pending_selections[confirmation_token] return response async def update_transaction_expense_data( extend: ExtendClient, transaction_id: str, user_confirmed_data_values: bool, data: Dict ) -> Dict: """ Internal function to update the expense data for a specific transaction. This should not be exposed directly to external callers. Args: extend: The Extend client instance transaction_id: The unique identifier of the transaction user_confirmed_data_values: Only true if the user has confirmed the specific values in the data argument data: A dictionary representing the expense data to update Returns: Dict: A dictionary containing the updated transaction details """ try: if not user_confirmed_data_values: raise Exception(f"User has not confirmed the expense category or label values") response = await extend.transactions.update_transaction_expense_data(transaction_id, data) return response except Exception as e: raise Exception(f"Error updating transaction expense data: {str(e)}") # ========================= # Receipt Attachment Functions # ========================= async def create_receipt_attachment( extend: ExtendClient, transaction_id: str, file_path: str, ) -> Dict: """ Create a receipt attachment by uploading a file via multipart form data. Args: extend: The Extend client instance transaction_id (str): The unique identifier of the transaction to attach the receipt to. file_path (str): A file path for the receipt image. Returns: Dict: A dictionary representing the receipt attachment details, including: - id: Unique identifier of the receipt attachment. - transactionId: The associated transaction ID. - contentType: The MIME type of the uploaded file. - urls: A dictionary with URLs for the original image, main image, and thumbnail. - createdAt: Timestamp when the receipt attachment was created. - uploadType: A string describing the type of upload (e.g., "TRANSACTION", "VIRTUAL_CARD"). """ try: with open(file_path, 'rb') as f: file_content = f.read() file_obj = io.BytesIO(file_content) # Get the filename and determine the MIME type filename = os.path.basename(file_path) mime_type = None # Set the MIME type based on file extension if filename.lower().endswith('.png'): mime_type = 'image/png' elif filename.lower().endswith('.jpg') or filename.lower().endswith('.jpeg'): mime_type = 'image/jpeg' elif filename.lower().endswith('.gif'): mime_type = 'image/gif' elif filename.lower().endswith('.bmp'): mime_type = 'image/bmp' elif filename.lower().endswith('.tif') or filename.lower().endswith('.tiff'): mime_type = 'image/tiff' elif filename.lower().endswith('.heic'): mime_type = 'image/heic' elif filename.lower().endswith('.pdf'): mime_type = 'application/pdf' else: raise ValueError(f"Unsupported file type: {filename}") file_obj = io.BytesIO(file_content) file_obj.name = filename file_obj.content_type = mime_type response = await extend.receipt_attachments.create_receipt_attachment( transaction_id=transaction_id, file=file_obj ) return response except Exception as e: logger.error("Error creating receipt attachment: %s", e) raise Exception("Error creating receipt attachment: %s", e) # ========================= # Receipt Capture Functions # ========================= async def automatch_receipts( extend: ExtendClient, receipt_attachment_ids: List[str], ) -> Dict: """ Initiates an asynchronous bulk receipt automatch job. This method triggers an asynchronous job on the server that processes the provided receipt attachment IDs. The operation is non-blocking: it immediately returns a job ID and preliminary details, while the matching process is performed in the background. Args: receipt_attachment_ids (List[str]): A list of receipt attachment IDs to be automatched. Returns: Dict: A dictionary representing the Bulk Receipt Automatch Response. """ try: response = await extend.receipt_capture.automatch_receipts( receipt_attachment_ids=receipt_attachment_ids ) return response except Exception as e: logger.error("Error initiating receipt automatch: %s", e) raise Exception("Error initiating receipt automatch: %s", e) async def get_automatch_status( extend: ExtendClient, job_id: str, ) -> Dict: """ Retrieves the status of a bulk receipt capture automatch job. Args: job_id (str): The ID of the automatch job whose status is to be retrieved. Returns: Dict: A dictionary representing the current Bulk Receipt Automatch Response. """ try: response = await extend.receipt_capture.get_automatch_status(job_id=job_id) return response except Exception as e: logger.error("Error getting automatch status: %s", e) raise Exception("Error getting automatch status: %s", e) async def send_receipt_reminder( extend: ExtendClient, transaction_id: str, ) -> Dict: """ Send a transaction-specific receipt reminder. Args: extend: The Extend client instance transaction_id (str): The unique identifier of the transaction. Returns: Dict: Response from the API indicating the reminder was sent successfully. """ try: response = await extend.transactions.send_receipt_reminder(transaction_id) return response except Exception as e: logger.error("Error sending receipt reminder: %s", e) raise Exception(f"Error sending receipt reminder: {e}") from e # Optional: Cleanup function to remove expired selections async def cleanup_pending_selections(): """Remove all expired selection tokens""" now = datetime.now() expired_tokens = [ token for token, selection in pending_selections.items() if now > datetime.fromisoformat(selection["expires_at"]) ] for token in expired_tokens: del pending_selections[token]

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/paywithextend/extend-ai-toolkit'

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