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
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions uploading files and an optional cashflow type, but fails to disclose critical traits like required permissions, rate limits, file format restrictions, or whether the operation is idempotent. For a bulk mutation tool, this lack of detail is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose, followed by clear sections for Args and Returns. Every sentence earns its place, with no redundant information, making it efficient and easy to parse for an agent.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description provides basic purpose and parameter semantics but lacks behavioral details (e.g., error handling, response format). For a bulk upload tool with mutation implications, this is minimally adequate but leaves gaps in understanding how to use it effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds meaningful context for both parameters: 'file_paths' is explained as 'List of paths to files to upload', and 'cashflow_type' is clarified with its optional nature and allowed values (INCOME or EXPENSE). Since schema description coverage is 0%, this compensates well, though it doesn't specify path format (e.g., local vs. remote).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Upload') and resource ('multiple file attachments in bulk'), distinguishing it from siblings like 'create_attachment' (singular) and 'list_attachments' (read-only). However, it doesn't explicitly differentiate from 'link_attachment_transaction', which might involve attachments but with a different focus.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus alternatives like 'create_attachment' (for single uploads) or 'link_attachment_transaction' (for linking existing attachments). The description lacks context about prerequisites, such as whether files must be pre-processed or if there are size limits, leaving the agent with no usage direction.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

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