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.
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.
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
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.
For this tutorial we're using the MakeLeaps REST API. MakeLeaps is a Japanese invoicing platform with a clean JSON API. You'll need:
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.
Here is the prompt we'll give Claude to generate our MCP server:
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.)
#!/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
Because the file starts with the uv script header, you can run it directly without installing anything globally. uv handles the dependencies automatically:
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.
To make Claude Desktop aware of your new MCP server, open (or create) the Claude Desktop configuration file:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonAdd your server under the mcpServers key
{ "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.
Once connected, you can have a natural conversation with Claude about your invoices. Here are some things you can now say:
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.
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 APImark_invoice_paid(invoice_id, payment_date) — record a payment against an invoicelist_overdue_invoices() — a convenience wrapper that filters for invoices past their due dateget_invoice_pdf(invoice_id) — download the PDF version of an invoiceImprovements:
list_invoices and list_clients for accounts with large data setsdecimal module to sanity-check amounts before sending them to the APITo 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.
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.