cd /news/ai-agents/build-a-small-business-invoice-agent… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-163571] src=dev.to β†— pub= topic=ai-agents verified=true sentiment=↑ positive

Build a Small-Business Invoice Agent with LangChain Deep Agents and MCP

A developer published a walkthrough for building a multi-agent invoice-triage system using LangChain's Deep Agents framework and the Model Context Protocol (MCP). The architecture pairs an orchestrator that plans and maintains a todo list with extractor and reviewer subagents, all accessing vendor lookups, invoice history, duplicate checks and file reading through a FastMCP server backed by SQLite, with path-traversal protection on file reads. The author notes the design is an extensible reference rather than a production system and advises pinning versions of deepagents, langchain-mcp-adapters and mcp.

by read6 min views1 publishedNov 4, 2026

Most "AI agent" tutorials stop at a chatbot that calls a weather API. Real small-business work looks different: a stack of PDFs in an inbox, a spreadsheet of vendors, and a person who has to decide what gets paid. In this post we'll build a small but realistic multi-agent system that triages incoming invoices, using two pieces that fit together well:

The goal isn't a production system in 1,200 words. It's a clean architecture you can extend. Library APIs move fast, so pin your versions and check the docs for deepagents, langchain-mcp-adapters and mcp before you copy this into anything serious.

A small company receives vendor invoices by email. For each one, someone has to:

None of this needs a model to "be creative". It needs reliable extraction, a few lookups, and a clear audit trail. That's a good fit for agents with narrow tools.

            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚   Orchestrator (Deep     β”‚
            β”‚   Agent: plans + todo)   β”‚
            β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚           β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”   β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ extractor  β”‚   β”‚  reviewer    β”‚
        β”‚ subagent   β”‚   β”‚  subagent    β”‚
        β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚  MCP tools    β”‚
        β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ MCP server: vendors, invoice β”‚
        β”‚ history, file reader         β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The orchestrator never touches the database directly. Everything goes through MCP tools, which means the same server can later serve a different agent framework, a desktop assistant, or an internal app.

Install the dependencies:

pip install "mcp[cli]" langchain-mcp-adapters deepagents langchain-openai

Here's a minimal MCP server using FastMCP. For the demo, the "database" is SQLite.

import sqlite3
from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("invoices")
DB = Path("business.db")

def q(sql: str, params: tuple = ()):
    with sqlite3.connect(DB) as conn:
        conn.row_factory = sqlite3.Row
        return [dict(r) for r in conn.execute(sql, params).fetchall()]

@mcp.tool()
def read_invoice_text(filename: str) -> str:
    """Return the extracted text of an invoice file in the inbox folder."""
    path = Path("inbox") / Path(filename).name  # prevent path traversal
    return path.read_text()

@mcp.tool()
def lookup_vendor(name: str) -> list[dict]:
    """Find known vendors whose name matches (case-insensitive)."""
    return q("SELECT id, name, payment_terms FROM vendors WHERE name LIKE ?",
             (f"%{name}%",))

@mcp.tool()
def invoice_history(vendor_id: int) -> list[dict]:
    """Past invoices for a vendor: number, date, total."""
    return q("SELECT number, date, total FROM invoices WHERE vendor_id = ? "
             "ORDER BY date DESC LIMIT 24", (vendor_id,))

@mcp.tool()
def is_duplicate(vendor_id: int, invoice_number: str) -> bool:
    """True if this vendor already has an invoice with this number."""
    return bool(q("SELECT 1 FROM invoices WHERE vendor_id = ? AND number = ?",
                  (vendor_id, invoice_number)))

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

A few deliberate choices:

Path(filename).name stops the model (or a malicious PDF) from reading files outside the inbox. You can poke at it with the MCP inspector: mcp dev invoice_server.py.

langchain-mcp-adapters turns MCP tools into regular LangChain tools:

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "invoices": {
        "command": "python",
        "args": ["invoice_server.py"],
        "transport": "stdio",
    }
})

tools = await client.get_tools()  # inside an async function
by_name = {t.name: t for t in tools}

Splitting by name matters. Each subagent should only see the tools it needs.

Deep Agents gives you a planning tool (a todo list), a virtual file system for scratch notes, and the ability to spawn subagents with their own prompt and toolset. Subagents keep the orchestrator's context clean: the extractor can wade through a messy invoice without dumping all of that text into the main conversation.

extractor = {
    "name": "extractor",
    "description": "Reads one invoice file and returns structured JSON.",
    "system_prompt": (
        "You extract invoice data. Call read_invoice_text, then return ONLY JSON "
        "with keys: vendor_name, invoice_number, date (YYYY-MM-DD), "
        "line_items (list of {description, amount}), total. "
        "If a field is missing, use null. Never guess numbers."
    ),
    "tools": [by_name["read_invoice_text"]],
}

reviewer = {
    "name": "reviewer",
    "description": "Checks extracted invoice JSON against vendor records.",
    "system_prompt": (
        "You review invoices. Given extracted JSON: look up the vendor, check for "
        "duplicates, and compare the total to that vendor's history. Report "
        "flags: UNKNOWN_VENDOR, DUPLICATE, TOTAL_MISMATCH (line items don't sum), "
        "UNUSUAL_AMOUNT (well above typical). Explain each flag in one sentence."
    ),
    "tools": [by_name["lookup_vendor"], by_name["invoice_history"],
              by_name["is_duplicate"]],
}

Note what's not delegated to the model: the definition of each flag. You decide the business rules. The model applies them and explains its reasoning.

import asyncio
from deepagents import create_deep_agent

ORCHESTRATOR_PROMPT = """You triage vendor invoices for a small business.
For each file you are given:
1. Plan the work with your todo list.
2. Delegate extraction to the `extractor` subagent.
3. Pass its JSON to the `reviewer` subagent.
4. Write a summary per invoice: vendor, number, total, flags, and a
   recommendation of APPROVE or NEEDS_HUMAN_REVIEW.
You never approve payments yourself."""

async def main(files: list[str]):
    agent = create_deep_agent(
        model="openai:gpt-4.1",      # any tool-calling model works
        tools=[],                      # orchestrator delegates everything
        system_prompt=ORCHESTRATOR_PROMPT,
        subagents=[extractor, reviewer],
    )
    result = await agent.ainvoke({
        "messages": [{"role": "user",
                      "content": f"Triage these invoices: {', '.join(files)}"}]
    })
    print(result["messages"][-1].content)

asyncio.run(main(["acme-1043.txt", "newco-0007.txt"]))

(In real code, create the MCP client and subagent dicts inside main so await client.get_tools() runs in the same event loop.)

The demo works. Here's what turns it into something a business can actually rely on:

Validate structured output. Don't trust the extractor's JSON. Parse it with Pydantic and send it back for a retry on failure:

from pydantic import BaseModel, model_validator

class LineItem(BaseModel):
    description: str
    amount: float

class Invoice(BaseModel):
    vendor_name: str
    invoice_number: str
    date: str
    line_items: list[LineItem]
    total: float

    @model_validator(mode="after")
    def totals_match(self):
        if abs(sum(i.amount for i in self.line_items) - self.total) > 0.01:
            raise ValueError("line items do not sum to total")
        return self

Arithmetic checks belong in code, not in a prompt.

Keep a human in the loop. The agent recommends, a person approves. Deep Agents supports interrupting before sensitive tool calls if you later add write tools like mark_approved.

Log everything. Store every tool call and result. When the owner asks "why was this flagged?", you can show them.

Evaluate with real samples. Gather 20–30 anonymized past invoices with known correct answers, and re-run them every time you change a prompt or model. That's your regression suite.

Treat documents as untrusted input. An invoice can contain text like "ignore previous instructions". Read-only tools and narrow subagent permissions limit what a prompt injection can do.

Swap "invoices" for insurance claims intake, real estate transaction documents, or permit applications, and the shape is the same: an MCP server wrapping your systems, an extractor, a reviewer that applies your rules, and an orchestrator that writes a decision-ready summary. That's the core of most intelligent document processing work. The hard parts are the business rules and the evaluation set, not the framework.

If you're deciding whether to build something like this or buy a SaaS tool, I wrote a longer comparison of custom AI vs. off-the-shelf solutions. For more workflow ideas beyond invoices, see 5 AI workflow automations every business needs.

Jimmy Hernandez is a senior AI engineer and founder of Business Edge Analytics, a Louisville, KY firm that builds multi-agent systems, RAG assistants and document automation for businesses and public-sector teams. He works with LangChain Deep Agents, MCP and Python, and has a background in insurance and real estate.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @langchain 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/build-a-small-busine…] indexed:0 read:6min 2026-11-04 Β· β€”