Skip to main content
Glama
norman-finance

Norman Finance MCP Server

Official

upload_bulk_attachments

Upload multiple file attachments in bulk to Norman Finance MCP Server. Supports optional cashflow type labeling (INCOME or EXPENSE) for streamlined financial workflows.

Instructions

Upload multiple file attachments in bulk. Args: file_paths: List of paths to files to upload cashflow_type: Optional cashflow type for the transactions (INCOME or EXPENSE) Returns: Response from the bulk upload request

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
cashflow_typeNo
file_pathsYes

Implementation Reference

  • Core handler implementation for upload_bulk_attachments tool. Processes list of file paths or URLs, downloads URLs to temp files if needed, validates paths, opens files, sends multipart POST to API endpoint /api/v1/accounting/transactions/upload-documents/, handles cleanup and errors.
    @mcp.tool() async def upload_bulk_attachments( ctx: Context, file_paths: List[str] = Field(description="List of paths or URLs to files to upload"), cashflow_type: Optional[str] = Field(description="Optional cashflow type for the transactions (INCOME or EXPENSE). If not provided, then try to detect it from the file") ) -> Dict[str, Any]: """ Upload multiple file attachments in bulk. Args: file_paths: List of paths or URLs to files to upload cashflow_type: Optional cashflow type for the transactions (INCOME or EXPENSE). If not provided, then try to detect it from the file Returns: Response from the bulk upload request """ api = ctx.request_context.lifespan_context["api"] company_id = api.company_id if not company_id: return {"error": "No company available. Please authenticate first."} # Validate cashflow_type if cashflow_type and cashflow_type not in ["INCOME", "EXPENSE"]: return {"error": "cashflow_type must be either 'INCOME' or 'EXPENSE'"} upload_url = urljoin( config.api_base_url, "api/v1/accounting/transactions/upload-documents/" ) temp_files = [] # Track temp files for cleanup opened_files = [] # Track opened file handles for cleanup try: files = [] valid_paths = [] # Process all file paths before proceeding for path in file_paths: if not validate_file_path(path): logger.warning(f"Invalid or unsafe file path: {path}") continue actual_path = path # Handle URLs by downloading them first if is_url(path): logger.info(f"Downloading file from URL: {path}") downloaded_path = download_file(path) if not downloaded_path: logger.warning(f"Failed to download file from URL: {path}") continue actual_path = downloaded_path temp_files.append(actual_path) logger.info(f"File downloaded to: {actual_path}") # Validate the file exists and is accessible if not os.path.exists(actual_path): logger.warning(f"File not found: {actual_path}") continue if not os.access(actual_path, os.R_OK): logger.warning(f"Permission denied when accessing file: {actual_path}") continue valid_paths.append(actual_path) if not valid_paths: return {"error": "No valid files found for upload"} # Open and prepare valid files for path in valid_paths: file_handle = open(path, "rb") opened_files.append(file_handle) files.append(("files", file_handle)) data = {} if cashflow_type: data["cashflow_type"] = cashflow_type response = api._make_request("POST", upload_url, json_data=data, files=files) # Close all opened file handles for file_handle in opened_files: file_handle.close() # Clean up temporary files for temp_file in temp_files: try: if os.path.exists(temp_file): os.remove(temp_file) os.rmdir(os.path.dirname(temp_file)) logger.info(f"Removed temporary file: {temp_file}") except Exception as e: logger.warning(f"Failed to remove temporary file {temp_file}: {str(e)}") return response except FileNotFoundError as e: return {"error": f"File not found: {str(e)}"} except PermissionError as e: return {"error": f"Permission denied when accessing file: {str(e)}"} except Exception as e: logger.error(f"Error uploading files: {str(e)}") return {"error": f"Error uploading files: {str(e)}"} finally: # Ensure files are closed and temp files are cleaned up in case of exceptions for file_handle in opened_files: try: file_handle.close() except Exception: pass for temp_file in temp_files: try: if os.path.exists(temp_file): os.remove(temp_file) os.rmdir(os.path.dirname(temp_file)) except Exception: pass
  • Top-level registration of document tools (including upload_bulk_attachments) by calling register_document_tools(server) in the main server setup.
    register_document_tools(server)
  • Import of register_document_tools function required for tool registration.
    from norman_mcp.tools.documents import register_document_tools
  • Pydantic schema definition for tool parameters using Field with descriptions.
    async def upload_bulk_attachments( ctx: Context, file_paths: List[str] = Field(description="List of paths or URLs to files to upload"), cashflow_type: Optional[str] = Field(description="Optional cashflow type for the transactions (INCOME or EXPENSE). If not provided, then try to detect it from the file") ) -> Dict[str, Any]:
  • Helper function to download files from URLs to temporary local paths for upload.
    def download_file(url: str) -> Optional[str]: """Download a file from URL to a temporary location and return its path.""" try: response = requests.get(url, stream=True, timeout=30) response.raise_for_status() # Extract filename from URL or Content-Disposition header filename = None if "Content-Disposition" in response.headers: # Try to get filename from Content-Disposition header content_disposition = response.headers["Content-Disposition"] match = re.search(r'filename="?([^"]+)"?', content_disposition) if match: filename = match.group(1) # If no filename found in header, extract from URL if not filename: url_path = urlparse(url).path filename = os.path.basename(url_path) or "downloaded_file" # Create a temporary file temp_dir = tempfile.mkdtemp(prefix="norman_") temp_path = os.path.join(temp_dir, filename) # Write the file with open(temp_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) return temp_path except Exception as e: logger.error(f"Error downloading file from {url}: {str(e)}") return None

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/norman-finance/norman-mcp-server'

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