Create an MCP Server For Your Company's Invoices

The Model Context Protocol (MCP) is an open standard that lets AI assistants like Claude connect directly to your internal tools and data sources. Instead of copying and pasting invoice data into a chat window, you can give Claude a live connection to your invoicing system — so it can look up, create, and manage invoices on your behalf, in plain English.

In this tutorial, we'll build an MCP server in Python that exposes your company's invoices to Claude. We'll use the MakeLeaps invoicing API as our example, but the same approach works with any invoicing system that has an HTTP API. By the end, you'll be able to ask Claude things like "Show me all unpaid invoices from last month" or "Create an invoice for Acme Corp for ¥150,000" and have it happen automatically.

This guide assumes basic knowledge of programming concepts and LLM usage. Even if you're fairly new to Python, you should be able to follow along.

What is MCP?

MCP (Model Context Protocol) is a standard introduced by Anthropic that allows AI models to call external tools in a structured, safe way. You define a server that exposes a set of tools — functions the AI can call — and then connect that server to Claude Desktop (or another MCP-compatible client). Claude can then decide when to call those tools during a conversation, passing arguments and receiving results back in real time.

Think of it like giving Claude a set of API endpoints it's allowed to use, along with plain-English descriptions of what each one does and what arguments it accepts. Claude handles the reasoning about when and how to call them.

Setting Up Your Claude Project

Before we write any code, let's set up a Claude Project so the LLM understands our coding conventions. This is the same approach used in the MakeLeaps invoice importer tutorial — we teach Claude our preferences up front, then ask it to write code for us.

Create a new Project in Claude and paste the following into the Project Instructions

Info
You write Python tools as single files. They always start with this comment:
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# ///
These files can include dependencies on libraries such as Click. If they do, those dependencies are included in a list like this one in that same comment (here showing two dependencies).
After this, include a comment with a short 1-line summary of the script followed by a paragraph description of what the script does.
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "click",
#     "sqlite-utils",
# ]
# ///

# An example one-line description of the script
#
# A paragraph about what the script does in
# more detail goes here...
#
When working with money or numbers intended to be input by users, use the decimal type and avoid using float.
For HTTP requests, use the httpx library.
For CLI tools, use click.
For MCP servers, use the fastmcp library.
When deploying, prefer uv instead of pip.

With this in place, Claude will produce code in the right style every time you ask.

Understanding the MakeLeaps API

For this tutorial we're using the MakeLeaps REST API. MakeLeaps is a Japanese invoicing platform with a clean JSON API. You'll need:

  • A MakeLeaps API Client ID and Client Secret (create these in the MakeLeaps API settings page)
  • Your Partner MID — a unique identifier for your MakeLeaps account

Authentication uses OAuth2's client_credentials flow. You post your credentials to get a short-lived Bearer token, then include that token in every subsequent request. We'll handle all of this inside our MCP server.

If you use a different invoicing system, the structure of the MCP server will be identical — you'll just swap out the API calls.

Writing the MCP Server

Here is the prompt we'll give Claude to generate our MCP server:

Info
Write a Python MCP server using fastmcp that connects to the MakeLeaps API and exposes the following tools:

list_invoices — returns a list of recent invoices, optionally filtered by status (unpaid, paid, overdue) and date range
get_invoice — takes an invoice ID and returns full details for that invoice
create_invoice — creates a new invoice given a client ID, line items (list of description + amount pairs), and a due date
list_clients — returns a list of clients so the user can find the right client ID before creating an invoice

The server should read MAKELEAPS_CLIENT_ID, MAKELEAPS_CLIENT_SECRET, and MAKELEAPS_PARTNER_MID from environment variables. It should handle OAuth2 client_credentials authentication with MakeLeaps automatically, refreshing the token when needed. Use httpx for HTTP requests and the decimal type for all monetary amounts. Follow the project's uv script conventions.

Claude will produce something like the following. (You can paste this directly — it's ready to run.)

Python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "fastmcp",
#     "httpx",
# ]
# ///

# MCP server exposing MakeLeaps invoice tools to Claude
#
# Connects to the MakeLeaps REST API and exposes tools for listing clients,
# listing invoices, fetching invoice details, and creating new invoices.
# Reads credentials from the environment variables MAKELEAPS_CLIENT_ID,
# MAKELEAPS_CLIENT_SECRET, and MAKELEAPS_PARTNER_MID. Handles OAuth2
# token acquisition and refresh automatically.
#

import os
import base64
from decimal import Decimal
from datetime import datetime, timedelta
from typing import Optional

import httpx
from fastmcp import FastMCP

mcp = FastMCP("MakeLeaps Invoices")

# ---------------------------------------------------------------------------
# Auth helpers
# ---------------------------------------------------------------------------

_token: Optional[str] = None
_token_expires_at: Optional[datetime] = None


def _get_credentials() -> tuple[str, str, str]:
    client_id = os.environ["MAKELEAPS_CLIENT_ID"]
    client_secret = os.environ["MAKELEAPS_CLIENT_SECRET"]
    partner_mid = os.environ["MAKELEAPS_PARTNER_MID"]
    return client_id, client_secret, partner_mid


def _fetch_token(client_id: str, client_secret: str) -> tuple[str, datetime]:
    raw = f"{client_id}:{client_secret}"
    encoded = base64.b64encode(raw.encode()).decode()
    with httpx.Client() as client:
        response = client.post(
            "https://api.makeleaps.com/user/oauth2/token/",
            headers={"Authorization": f"Basic {encoded}"},
            data={"grant_type": "client_credentials"},
        )
        response.raise_for_status()
        data = response.json()
    token = data["access_token"]
    expires_in = int(data.get("expires_in", 3600))
    expires_at = datetime.utcnow() + timedelta(seconds=expires_in - 60)
    return token, expires_at


def _auth_header() -> dict[str, str]:
    global _token, _token_expires_at
    client_id, client_secret, _ = _get_credentials()
    if _token is None or datetime.utcnow() >= (_token_expires_at or datetime.min):
        _token, _token_expires_at = _fetch_token(client_id, client_secret)
    return {"Authorization": f"Bearer {_token}"}


def _api_get(path: str, params: dict | None = None) -> dict:
    _, _, partner_mid = _get_credentials()
    url = f"https://api.makeleaps.com/api/partner/{partner_mid}{path}"
    with httpx.Client() as client:
        response = client.get(url, headers=_auth_header(), params=params or {})
        response.raise_for_status()
        return response.json()["response"]


def _api_post(path: str, data: dict) -> dict:
    _, _, partner_mid = _get_credentials()
    url = f"https://api.makeleaps.com/api/partner/{partner_mid}{path}"
    with httpx.Client() as client:
        response = client.post(url, headers=_auth_header(), json=data)
        response.raise_for_status()
        return response.json()["response"]


# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------


@mcp.tool()
def list_clients() -> list[dict]:
    """List all clients in MakeLeaps. Use this to find a client's ID before
    creating an invoice."""
    result = _api_get("/client/")
    return [
        {
            "id": c["mid"],
            "name": c.get("name", "(unnamed)"),
            "external_id": c.get("client_external_id"),
        }
        for c in result.get("results", [])
    ]


@mcp.tool()
def list_invoices(
    status: Optional[str] = None,
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
) -> list[dict]:
    """List invoices, optionally filtered by status and date range.

    Args:
        status: Optional filter — one of 'paid', 'unpaid', or 'overdue'.
        date_from: Optional start date in YYYY-MM-DD format.
        date_to: Optional end date in YYYY-MM-DD format.
    """
    params: dict = {"document_type": "invoice"}
    if status:
        params["status"] = status
    if date_from:
        params["date__gte"] = date_from
    if date_to:
        params["date__lte"] = date_to

    result = _api_get("/document/", params=params)
    return [
        {
            "id": doc["mid"],
            "number": doc.get("document_number"),
            "date": doc.get("date"),
            "client": doc.get("client_name"),
            "total": str(Decimal(str(doc.get("total", "0")))),
            "currency": doc.get("currency"),
            "status": doc.get("status"),
        }
        for doc in result.get("results", [])
    ]


@mcp.tool()
def get_invoice(invoice_id: str) -> dict:
    """Get full details for a single invoice.

    Args:
        invoice_id: The MID of the invoice to retrieve.
    """
    doc = _api_get(f"/document/{invoice_id}/")
    return {
        "id": doc["mid"],
        "number": doc.get("document_number"),
        "date": doc.get("date"),
        "due_date": doc.get("payment_due_date"),
        "client": doc.get("client_name"),
        "status": doc.get("status"),
        "currency": doc.get("currency"),
        "subtotal": str(Decimal(str(doc.get("subtotal", "0")))),
        "tax": str(Decimal(str(doc.get("tax", "0")))),
        "total": str(Decimal(str(doc.get("total", "0")))),
        "line_items": [
            {
                "description": item.get("description"),
                "quantity": item.get("quantity"),
                "price": str(Decimal(str(item.get("price", "0")))),
            }
            for item in doc.get("lineitems", [])
        ],
    }


@mcp.tool()
def create_invoice(
    client_id: str,
    line_items: list[dict],
    due_date: str,
    currency: str = "JPY",
    document_number: Optional[str] = None,
) -> dict:
    """Create a new invoice in MakeLeaps.

    Args:
        client_id: The MID of the client to invoice.
        line_items: A list of dicts, each with 'description' (str) and
                    'amount' (str or int). Example:
                    [{"description": "Consulting - June", "amount": "150000"}]
        due_date: Payment due date in YYYY-MM-DD format.
        currency: Currency code, defaults to 'JPY'.
        document_number: Optional custom invoice number. Auto-assigned if omitted.
    """
    items = [
        {
            "kind": "simple",
            "description": item["description"],
            "price": str(Decimal(str(item["amount"]))),
        }
        for item in line_items
    ]

    total = sum(Decimal(str(item["amount"])) for item in line_items)

    payload: dict = {
        "document_type": "invoice",
        "document_template": "ja_JP_pro_4",
        "date": datetime.today().strftime("%Y-%m-%d"),
        "payment_due_date": due_date,
        "client": client_id,
        "lineitems": items,
        "currency": currency,
        "total": str(total),
        "subtotal": str(total),
        "tax": "0",
    }
    if document_number:
        payload["document_number"] = document_number

    result = _api_post("/document/", payload)
    return {
        "id": result["mid"],
        "number": result.get("document_number"),
        "total": str(total),
        "currency": currency,
        "status": result.get("status"),
    }


if __name__ == "__main__":
    mcp.run()

Save this file as makeleaps_mcp.py

Running the Server

Because the file starts with the uv script header, you can run it directly without installing anything globally. uv handles the dependencies automatically:

Bash
export MAKELEAPS_CLIENT_ID="your_client_id"
export MAKELEAPS_CLIENT_SECRET="your_client_secret"
export MAKELEAPS_PARTNER_MID="your_partner_mid"

chmod +x makeleaps_mcp.py
./makeleaps_mcp.py

You should see output confirming the MCP server is running and listening.

Connecting to Claude Desktop

To make Claude Desktop aware of your new MCP server, open (or create) the Claude Desktop configuration file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add your server under the mcpServers key

JSON
{
  "mcpServers": {
    "makeleaps": {
      "command": "/path/to/makeleaps_mcp.py",
      "env": {
        "MAKELEAPS_CLIENT_ID": "your_client_id",
        "MAKELEAPS_CLIENT_SECRET": "your_client_secret",
        "MAKELEAPS_PARTNER_MID": "your_partner_mid"
      }
    }
  }
}

Replace /path/to/makeleaps_mcp.py with the absolute path to the file you saved. Restart Claude Desktop, and a small hammer icon should appear in the interface, indicating that tools are available.

Trying It Out

Once connected, you can have a natural conversation with Claude about your invoices. Here are some things you can now say:

  • "List all unpaid invoices from this month."
  • "Who are our current clients?"
  • "Create an invoice for client ABC-123 for consulting services at ¥200,000, due in 30 days."
  • "Show me the line items on invoice INV-0042."
  • "How many invoices did we issue last quarter, and what was the total value?"

Claude will call the appropriate tools, handle the API responses, and present the results conversationally. If it needs to chain multiple calls together — for example, looking up a client ID before creating an invoice — it will do that automatically.

Extending the Server

This is a starting point. Here are some natural next steps to prompt Claude to add:

More tools to add:

  • send_invoice(invoice_id) — trigger a secure email send via the MakeLeaps Sending Order API
  • mark_invoice_paid(invoice_id, payment_date) — record a payment against an invoice
  • list_overdue_invoices() — a convenience wrapper that filters for invoices past their due date
  • get_invoice_pdf(invoice_id) — download the PDF version of an invoice

Improvements:

  • Add pagination support to list_invoices and list_clients for accounts with large data sets
  • Cache the client list locally to reduce API calls when creating multiple invoices in one session
  • Add input validation that uses Python's decimal module to sanity-check amounts before sending them to the API

To generate any of these, simply describe what you want to the Claude Project you set up earlier. Because Claude already knows your coding conventions and the structure of the existing server, it will produce code that fits right in.

Wrapping Up

We've used an LLM to help write a working MCP server, and then connected that server to Claude Desktop so that Claude can manage invoices through natural conversation. The whole thing is a single Python file that runs with no manual dependency management thanks to uv.

If you get stuck on any step, try describing the problem to Claude in your Project — since it already knows your code and conventions, it's well positioned to help you debug. The same approach applies when you want to add new tools or adapt this for a different API.

As LLMs get better at tool use and reasoning over multi-step tasks, integrations like this one become increasingly powerful. A good MCP server is a force multiplier: write it once, and every capability you expose is immediately available to every future conversation.