Skip to main content
Glama
norman-finance

Norman Finance MCP Server

Official

create_attachment

Generate and link attachments to transactions by uploading files, specifying type (e.g., invoice, receipt), and adding metadata such as amount, currency, VAT, and supplier details in Norman Finance systems.

Instructions

Create a new attachment.

Args:
    file_path: Path to file to upload
    transactions: List of transaction IDs to link
    attachment_type: Type of attachment (invoice, receipt)
    amount: Amount related to attachment
    amount_exchanged: Exchanged amount in different currency
    attachment_number: Unique number for attachment
    brand_name: Brand name associated with attachment
    currency: Currency of amount (default EUR)
    currency_exchanged: Exchanged currency (default EUR)
    description: Description of attachment
    supplier_country: Country of supplier (DE, INSIDE_EU, OUTSIDE_EU)
    value_date: Date of value
    vat_sum_amount: VAT sum amount
    vat_sum_amount_exchanged: Exchanged VAT sum amount
    vat_rate: VAT rate percentage
    sale_type: Type of sale
    additional_metadata: Additional metadata for attachment
    
Returns:
    Created attachment information

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
additional_metadataNo
amountNo
amount_exchangedNo
attachment_numberNo
attachment_typeNo
brand_nameNo
currencyNoEUR
currency_exchangedNoEUR
descriptionNo
file_pathYes
sale_typeNo
supplier_countryNo
transactionsNo
value_dateNo
vat_rateNo
vat_sum_amountNo
vat_sum_amount_exchangedNo

Implementation Reference

  • Core implementation of create_attachment tool: validates input, downloads URL if needed, uploads file to Norman API with metadata, handles cleanup and errors.
    async def create_attachment(
        ctx: Context,
        file_path: str = Field(description="Path or URL to file to upload"),
        transactions: Optional[List[str]] = Field(description="List of transaction IDs to link"),
        attachment_type: Optional[str] = Field(description="Type of attachment (invoice, receipt)"),
        amount: Optional[float] = Field(description="Amount related to attachment"),
        amount_exchanged: Optional[float] = Field(description="Exchanged amount in different currency"),
        attachment_number: Optional[str] = Field(description="Unique number for attachment"),
        brand_name: Optional[str] = Field(description="Brand name associated with attachment"),
        currency: str = "EUR",
        currency_exchanged: str = "EUR",
        description: Optional[str] = Field(description="Description of attachment"),
        supplier_country: Optional[str] = Field(description="Country of supplier (DE, INSIDE_EU, OUTSIDE_EU)"),
        value_date: Optional[str] = Field(description="Date of value"),
        vat_sum_amount: Optional[float] = Field(description="VAT sum amount"),
        vat_sum_amount_exchanged: Optional[float] = Field(description="Exchanged VAT sum amount"),
        vat_rate: Optional[int] = Field(description="VAT rate percentage"),
        sale_type: Optional[str] = Field(description="Type of sale"),
        additional_metadata: Optional[Dict[str, Any]] = Field(description="Additional metadata for attachment")
    ) -> Dict[str, Any]:
        """
        Create a new attachment.
        
        Args:
            file_path: Path to file or URL to upload
            transactions: List of transaction IDs to link
            attachment_type: Type of attachment (invoice, receipt)
            amount: Amount related to attachment
            amount_exchanged: Exchanged amount in different currency
            attachment_number: Unique number for attachment
            brand_name: Brand name associated with attachment
            currency: Currency of amount (default EUR)
            currency_exchanged: Exchanged currency (default EUR)
            description: Description of attachment
            supplier_country: Country of supplier (DE, INSIDE_EU, OUTSIDE_EU)
            value_date: Date of value
            vat_sum_amount: VAT sum amount
            vat_sum_amount_exchanged: Exchanged VAT sum amount
            vat_rate: VAT rate percentage
            sale_type: Type of sale
            additional_metadata: Additional metadata for attachment
            
        Returns:
            Created attachment information
        """
        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 file path
        if not validate_file_path(file_path):
            return {"error": "Invalid or unsafe file path"}
            
        # Validate attachment_type    
        if attachment_type and attachment_type not in ["invoice", "receipt", "contract", "other"]:
            return {"error": "attachment_type must be one of: invoice, receipt, contract, other"}
        
        # Validate supplier_country
        if supplier_country and supplier_country not in ["DE", "INSIDE_EU", "OUTSIDE_EU"]:
            return {"error": "supplier_country must be one of: DE, INSIDE_EU, OUTSIDE_EU"}
            
        # Validate sale_type
        if sale_type and sale_type not in ["GOODS", "SERVICES"]:
            return {"error": "sale_type must be one of: GOODS, SERVICES"}
            
        attachments_url = urljoin(
            config.api_base_url,
            f"api/v1/companies/{company_id}/attachments/"
        )
        
        try:
            temp_file_path = None
            actual_file_path = file_path
            
            # Check if file_path is a URL and download it if needed
            if is_url(file_path):
                logger.info(f"Downloading file from URL: {file_path}")
                temp_file_path = download_file(file_path)
                if not temp_file_path:
                    return {"error": f"Failed to download file from URL: {file_path}"}
                actual_file_path = temp_file_path
                logger.info(f"File downloaded to: {actual_file_path}")
            
            # Check if file exists and is readable
            if not os.path.exists(actual_file_path):
                return {"error": f"File not found: {actual_file_path}"}
                
            if not os.access(actual_file_path, os.R_OK):
                return {"error": f"Permission denied when accessing file: {actual_file_path}"}
                
            files = {
                "file": open(actual_file_path, "rb")
            }
                
            data = {}
            if transactions:
            # Validate each transaction ID
                data["transactions"] = [tx for tx in transactions if validate_input(tx)]
            if attachment_type:
                data["attachment_type"] = attachment_type
            if amount is not None:
                data["amount"] = amount
            if amount_exchanged is not None:
                data["amount_exchanged"] = amount_exchanged
            if attachment_number:
                data["attachment_number"] = validate_input(attachment_number)
            if brand_name:
                data["brand_name"] = brand_name
            if currency:
                data["currency"] = currency
            if currency_exchanged:
                data["currency_exchanged"] = currency_exchanged
            if description:
                data["description"] = description
            if supplier_country:
                data["supplier_country"] = supplier_country
            if value_date:
                data["value_date"] = value_date
            if vat_sum_amount is not None:
                data["vat_sum_amount"] = vat_sum_amount
            if vat_sum_amount_exchanged is not None:
                data["vat_sum_amount_exchanged"] = vat_sum_amount_exchanged
            if vat_rate is not None:
                data["vat_rate"] = vat_rate
            if sale_type:
                data["sale_type"] = sale_type
            if additional_metadata:
                # Sanitize the metadata
                sanitized_metadata = {}
                for key, value in additional_metadata.items():
                    if isinstance(value, str):
                        sanitized_metadata[validate_input(key)] = validate_input(value)
                    else:
                        sanitized_metadata[validate_input(key)] = value
                data["additional_metadata"] = sanitized_metadata
                
            response = api._make_request("POST", attachments_url, json_data=data, files=files)
            
            # Close the file handle
            files["file"].close()
            
            # Clean up temporary file if we downloaded one
            if temp_file_path and os.path.exists(temp_file_path):
                try:
                    os.remove(temp_file_path)
                    os.rmdir(os.path.dirname(temp_file_path))
                    logger.info(f"Removed temporary file: {temp_file_path}")
                except Exception as e:
                    logger.warning(f"Failed to remove temporary file: {str(e)}")
                    
            return response
        except FileNotFoundError:
            return {"error": f"File not found: {file_path}"}
        except PermissionError:
            return {"error": f"Permission denied when accessing file: {file_path}"}
        except Exception as e:
            # Clean up temporary file if there was an error
            if temp_file_path and os.path.exists(temp_file_path):
                try:
                    os.remove(temp_file_path)
                    os.rmdir(os.path.dirname(temp_file_path))
                except Exception:
                    pass
            logger.error(f"Error uploading file: {str(e)}")
            return {"error": f"Error uploading file: {str(e)}"}
  • Pydantic Field descriptions define the input schema and documentation for the tool parameters.
    ctx: Context,
    file_path: str = Field(description="Path or URL to file to upload"),
    transactions: Optional[List[str]] = Field(description="List of transaction IDs to link"),
    attachment_type: Optional[str] = Field(description="Type of attachment (invoice, receipt)"),
    amount: Optional[float] = Field(description="Amount related to attachment"),
    amount_exchanged: Optional[float] = Field(description="Exchanged amount in different currency"),
    attachment_number: Optional[str] = Field(description="Unique number for attachment"),
    brand_name: Optional[str] = Field(description="Brand name associated with attachment"),
    currency: str = "EUR",
    currency_exchanged: str = "EUR",
    description: Optional[str] = Field(description="Description of attachment"),
    supplier_country: Optional[str] = Field(description="Country of supplier (DE, INSIDE_EU, OUTSIDE_EU)"),
    value_date: Optional[str] = Field(description="Date of value"),
    vat_sum_amount: Optional[float] = Field(description="VAT sum amount"),
    vat_sum_amount_exchanged: Optional[float] = Field(description="Exchanged VAT sum amount"),
    vat_rate: Optional[int] = Field(description="VAT rate percentage"),
    sale_type: Optional[str] = Field(description="Type of sale"),
    additional_metadata: Optional[Dict[str, Any]] = Field(description="Additional metadata for attachment")
  • Top-level registration by calling register_document_tools on the MCP server instance, which defines and registers the create_attachment tool.
    register_document_tools(server)
  • The @mcp.tool() decorator registers the create_attachment function when register_document_tools is called.
    @mcp.tool()
  • 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 full burden but offers minimal behavioral insight. It mentions that the tool 'Create[s] a new attachment' and returns 'Created attachment information', but doesn't disclose critical traits like whether this is a mutating operation (implied by 'Create'), error conditions, authentication requirements, rate limits, or side effects. The description is too basic for a tool with 17 parameters and no annotation support.

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

Conciseness3/5

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

The description is appropriately front-loaded with the core purpose, but the parameter list is extremely verbose (17 items). While each parameter explanation is brief, the overall structure feels bloated. The 'Returns' section is minimal but adequate. Some parameters could potentially be grouped or explained more efficiently.

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 the complexity (17 parameters, no annotations, no output schema), the description provides excellent parameter semantics but lacks crucial behavioral context. It doesn't explain the return format beyond 'Created attachment information', error handling, or system constraints. For a creation tool with financial data implications, more guidance on validation, constraints, and typical usage patterns would be beneficial.

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

Parameters5/5

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

The description provides extensive parameter documentation with clear explanations for all 17 parameters, far exceeding the 0% schema description coverage. Each parameter is listed with a brief semantic explanation (e.g., 'Path to file to upload', 'Type of attachment (invoice, receipt)', 'Country of supplier (DE, INSIDE_EU, OUTSIDE_EU)'), adding significant value beyond the bare schema.

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 verb 'Create' and resource 'attachment', making the purpose unambiguous. It distinguishes from siblings like 'upload_bulk_attachments' by focusing on single attachment creation, though it doesn't explicitly compare to other attachment-related tools like 'list_attachments' or 'link_attachment_transaction'.

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 'upload_bulk_attachments' for multiple files or 'link_attachment_transaction' for linking existing attachments. The description lacks context about prerequisites, such as needing existing transactions to link, or when this tool is appropriate compared to other creation tools like 'create_invoice'.

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