InvoiceLens MCP
Uses Azure OpenAI for text embeddings and LLM-based invoice extraction, enabling semantic document search and structured invoice parsing, with offline fallbacks when not configured.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@InvoiceLens MCP extract the invoice data from invoice.pdf and validate the totals"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
InvoiceLens MCP
An MCP server for invoice PDFs and vendor research. It exposes five tools that any MCP client can use: a LangGraph agent in another project, Claude Desktop, or Claude Code.
Tool | What it does |
| Parses a PDF, hybrid-chunks it (tables, header fields, totals and prose handled separately), embeds the chunks with |
| Semantic search across everything ingested; returns passages with |
| Reads one invoice into a typed schema, then validates the arithmetic (line items vs subtotal, subtotal + tax vs total, tax rate, dates) |
| Share price, market cap, sector, CEO and 3 years of revenue, net income and EPS growth for a listed company (Financial Modeling Prep) |
| Current web results with source URLs and dates: news, growth, strategy (Tavily) |
The same operations are also exposed as a REST API (/ingest, /search, /extract, /company-financials, /web-search) from the same process, backed by the same index.
Architecture
flowchart TB
subgraph clients[Clients]
LG[LangGraph agent<br/>any project]
CD[Claude Desktop / Code<br/>stdio]
HC[HTTP client<br/>curl, Swagger UI]
end
subgraph server[InvoiceLens server]
MCP[MCP server · FastMCP<br/>5 tools, stdio + HTTP]
API[REST API · FastAPI<br/>/ingest /search /extract …]
SVC[InvoiceLensService<br/>single shared core]
ING[Ingestion<br/>parse → hybrid chunk → embed]
RET[Retriever<br/>query → top-k]
EXT[Extractor<br/>LLM + rules fallback → validator]
MKT[Market data + web search<br/>cached, actionable errors]
FAISS[(FAISS IndexFlatIP<br/>per collection, on disk)]
META[(chunk + document<br/>metadata, original PDFs)]
end
AOAI[Azure OpenAI<br/>embedding-3-large · gpt-4o]
EXTAPI[FMP · Tavily]
LG --> MCP
CD --> MCP
HC --> API
MCP --> SVC
API --> SVC
SVC --> ING & RET & EXT & MKT
ING --> FAISS & META
RET --> FAISS
EXT --> META
ING & RET & EXT -.-> AOAI
MKT -.-> EXTAPIBoth interfaces are thin adapters over one InvoiceLensService, so they can't drift apart. A test asserts that MCP and REST return identical results for the same call.
The agent graph (agent/)
flowchart LR
S([start]) --> A[agent<br/>LLM + 5 MCP tools]
A -- tool calls --> T[tools<br/>MCP ToolNode]
A -- no tool calls --> E([answer, cited])
T -- extract_invoice --> V{{validate<br/>plain code}}
T -- other tools --> A
V -- valid, or retry with errors --> A
V -- failed twice --> R[flag for review] --> E2([needs human review])The validate node is deterministic code, not an LLM. When an extraction fails the arithmetic checks, it sends the exact errors back to the agent ("Line items sum to 5,700.00 but subtotal is 5,900.00"). The agent then re-extracts with that feedback. If the second attempt also fails, the graph stops and flags the invoice for human review, rather than letting the agent present numbers that don't add up. Some invoices really are wrong; samples/initech_* is one.
Related MCP server: doc-intel MCP server
Vendor research: market data and web search
Two tools look outward, so an agent can go from "who sent this invoice?" to "what do we know about them?"
Question | Tool | Why this one |
"What's Microsoft's market cap / share price / revenue growth?" |
| Structured, dated numbers from a market-data API |
"How is the company growing? Any recent news?" |
| Recent context with source URLs |
Numbers deliberately don't come from web search: snippets can be days old or quote the wrong currency. The tool descriptions and the agent's system prompt both steer the model to the right tool.
Setup. Both keys are optional; without one, its tool returns a clear "set INVOICELENS_…_API_KEY" error and everything else keeps working.
INVOICELENS_TAVILY_API_KEY=tvly-... # https://app.tavily.com
INVOICELENS_FMP_API_KEY=... # https://site.financialmodelingprep.comcurl "http://127.0.0.1:8765/company-financials?company=Microsoft"
curl -H 'content-type: application/json' -d '{"query":"Microsoft cloud growth","topic":"news","time_range":"month"}' \
http://127.0.0.1:8765/web-searchKnow the free tiers (as of October 2026):
FMP: 250 calls a day, and US-listed securities only. Non-US companies resolve through a US listing where one exists (Infosys →
INFYADR); otherwise they need a paid plan. Name searches prefer US exchanges for this reason. Lookups are cached for 10 minutes (one company costs 2–3 calls), and if the growth endpoint isn't in your plan, the profile still returns with a note.Tavily: 1,000 credits a month on the free tier. Each search uses one credit (
search_depth: basic).Private companies, like the fictional vendors in
samples/, aren't listed. The financials tool says so and points the agent to web search instead. To demo the market tools, ask about a real US-listed company.
Market data is returned with its source, retrieved_at timestamp and a "not investment advice" note, and the API key travels in a header, never in the URL.
Hybrid chunking
A pure fixed-size splitter cuts invoices in the worst places: through a line-item table, or between Invoice Number: and its value. A pure structural splitter makes wildly uneven chunks. InvoiceLens does both, in order.
1. Structure. Each page's elements are classified in reading order:
Element | Becomes |
Table | Its own chunk, as a markdown table. Never split mid-row; oversized tables split by rows with the header repeated in each piece |
| A |
Vendor, address, invoice no., dates, bill-to | Merged into one |
Short title line | Not a chunk; becomes the |
Paragraphs | Go to step 2 |
2. Size. Prose is packed into windows of at most 512 tokens with 64 tokens of sentence-level overlap. Sentences are never cut unless a single sentence exceeds the window.
Every chunk is embedded with a contextual header (Document: acme.pdf | Page 1 | totals | Section: …), so a terms paragraph is still findable by its vendor.
On the samples, every invoice produces header → table → totals → text. The Northwind terms page (about 180 tokens) is one text chunk under Section: Terms and Conditions; longer documents are windowed with overlap (see test_long_prose_is_windowed_with_overlap).
Quick start
pip install -e ".[agent,dev]"
cp .env.example .env # add Azure OpenAI details for real embeddings + LLM extraction
invoicelens-serve # REST + MCP on http://127.0.0.1:8765 (Swagger at /docs)curl -F file=@samples/northwind_logistics_2026-04.pdf http://127.0.0.1:8765/ingest
curl -H 'content-type: application/json' -d '{"query":"late payment penalty"}' http://127.0.0.1:8765/searchWith no keys set, InvoiceLens runs fully offline using a hashing embedder and a rule-based extractor. That's how the tests and CI run, and why the server always starts. The hashing embedder is lexical, not semantic, so use Azure OpenAI for real retrieval quality. /health reports which providers are active.
Use it from another project
That project needs no InvoiceLens code. It only needs the server's URL and langchain-mcp-adapters:
from langchain.agents import create_agent # pip install langchain langchain-mcp-adapters
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"invoicelens": {
"url": "http://127.0.0.1:8765/mcp",
"transport": "streamable_http",
# "headers": {"Authorization": "Bearer <INVOICELENS_API_TOKEN>"},
}
})
tools = await client.get_tools() # the five invoicelens_* tools
agent = create_agent(llm, tools)To get the self-correcting validation loop, copy agent/graph.py instead and call build_graph(llm, tools). Or run the bundled one:
python -m agent.run "What is the total on the Acme invoice?"
python -m agent.run --stdio "Which invoice charges a late payment penalty?" # launches the server itselfClaude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"invoicelens": { "command": "invoicelens-mcp", "args": ["--transport", "stdio"] }
}
}Things to know when sharing one server
One index, many clients. Everything ingested is searchable by every client. Pass
collection(e.g."client-a") to keep separate indexes.Sending files. Over stdio, tools can take a server-side
path. A remote server can't see your disk, so sendpdf_base64, and setINVOICELENS_ALLOW_LOCAL_PATHS=falseon any networked deployment.Auth. Bound to
127.0.0.1, the MCP SDK's DNS-rebinding protection only accepts localhostHostheaders. Bound to0.0.0.0(Docker, cloud) it doesn't, so setINVOICELENS_API_TOKENto requireAuthorization: Bearer …on REST and MCP. For multi-user production, put it behind an OAuth gateway such as Azure API Management with Entra ID.Keys stay on the server. Azure OpenAI credentials live in the server's
.envonly; client projects never see them.
Design decisions and safety nets
Decision | Why |
| Re-ingesting the same file replaces it instead of duplicating its chunks |
Index manifest records the embedding model | Mixing vectors from two models silently ruins search; the store refuses with an explicit error |
Extractor reports values as printed | Silently "fixing" a wrong invoice would hide a real problem from whoever pays it; the validator flags it instead |
LLM extraction falls back to rules on failure | A network blip degrades quality, not availability; |
Exact | Exact cosine search, fast well past demo scale (~100k chunks); swap for HNSW or a managed store beyond that |
Tool errors say what to do next | The agent reads them first: "No document 'x'. Use invoicelens_search_documents to find doc_ids" |
Version notes
The project pins MCP SDK 1.x (mcp>=1.30,<2) on purpose. MCP SDK 2.x renamed FastMCP to MCPServer and changed internals, and langchain-mcp-adapters (the LangGraph bridge, 0.3.2 at the time of writing) doesn't support 2.x yet. Pinning 1.x keeps the server usable from any LangGraph project today.
Tests
pytest -q # 74 tests, offline, ~8 sArea | What's covered |
Chunking | tables never split mid-row (header repeated), prose windows with overlap, nothing dropped, vendor kept in the header chunk |
Store | re-ingest replaces, persistence across restarts, filters, collection isolation, refuses mixed embedding models |
Extraction | all sample invoices field by field, the planted Initech error, each validator rule, date formats, LLM fallback |
Interfaces | MCP tool schemas and annotations, actionable errors, REST endpoints, MCP–REST parity, bearer auth, DNS-rebinding protection |
Agent | the real MCP server with a scripted LLM: a valid invoice goes straight to the answer; an invalid one gets the validator's errors fed back, then is flagged after two failures |
Providers | Azure/OpenAI paths against SDK-shaped fakes: batching, dimension check, feedback in the prompt |
External data | Tavily and FMP against mocked HTTP: request shape, US-listing resolution, plan-restricted growth, caching, every error path, missing keys as tool errors |
Project layout
src/invoicelens/
parsing.py PDF -> text blocks + tables, in reading order
chunking.py hybrid chunker
embeddings.py Azure OpenAI / OpenAI text-embedding-3-large, offline hashing embedder
store.py FAISS + metadata per collection, persisted
extraction.py LLM structured outputs, rule-based fallback
validation.py arithmetic and date checks
service.py the shared core
external.py Tavily web search, FMP market data
mcp_server.py FastMCP: 5 tools, stdio + Streamable HTTP
api.py FastAPI + MCP mounted at /mcp
agent/ LangGraph agent (an MCP client; imports nothing from invoicelens)
samples/ synthetic invoices + their generator
tests/Sample data
samples/ holds four synthetic invoices generated by samples/generate_samples.py. No real company data is included. Initech's stated subtotal is deliberately wrong, and Northwind has a terms-and-conditions page.
Available Tools
5 toolsinvoicelens_company_financialsARead-onlyIdempotent
Share price, market capitalisation, sector/industry, CEO, employees and annual growth (revenue, net income, EPS; fractions, 0.12 = +12%) for a publicly listed company, from Financial Modeling Prep.
Check resolved_by: if it was found by name search, confirm name matches what was
meant. Private companies aren't covered. Quote retrieved_at with the numbers; prices may
be delayed. For context or news, use invoicelens_web_search.
Inputs: company (required), include_growth. Returns: ceo, beta, name, notes, +20 more
invoicelens_extract_invoiceARead-onlyIdempotent
Extract one invoice into structured fields (vendor, invoice number, dates, currency, line items, subtotal, tax, total) and check them: line items vs subtotal, subtotal + tax vs total, tax rate, dates.
validation.valid = false means the numbers don't add up — either a misread (retry with
feedback) or an invoice that is genuinely wrong. Values are reported as printed, never
silently corrected.
Inputs: path, doc_id, feedback, collection, +1 more. Returns: doc_id, invoice, filename, extractor, +1 more
invoicelens_ingest_documentAIdempotent
Parse a PDF, split it with hybrid chunking (tables, header fields, totals and text kept as separate chunks), embed the chunks and store them in the FAISS index.
Ingesting the same file again replaces its previous version (doc_id is a content hash). Returns the doc_id plus chunk counts by type.
Inputs: path, filename, collection, pdf_base64. Returns: pages, chunks, doc_id, embedder, +4 more
invoicelens_search_documentsARead-onlyIdempotent
Semantic search over ingested invoices. Returns the top-k passages with doc_id, filename, page, chunk type and similarity score — cite filename and page in answers. Use the doc_id with invoicelens_extract_invoice to read a document's fields precisely.
Inputs: query (required), k, doc_id, collection, +1 more. Returns: query, results, collection
invoicelens_web_searchARead-only
Search the web (Tavily) and return ranked results with title, URL, snippet and publication date. Use for recent news, growth narrative and strategy; cite the URLs. Don't take share prices or market caps from snippets — use invoicelens_company_financials for numbers.
Inputs: query (required), topic, time_range, max_results, +1 more. Returns: query, topic, answer, results, +2 more
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 5 tools
Most tools are clearly distinct: ingest stores documents, extract reads structured fields with validation, company_financials pulls market data, and web_search queries the internet. The only potential confusion pairs are ingest vs. extract (both document-processing) and search_documents vs. web_search, but the descriptions explicitly delineate their roles and cross-reference each other.
All names are snake_case with a consistent invoicelens_ prefix, and the first three follow a verb_noun pattern (ingest_document, extract_invoice, search_documents). The last two (company_financials, web_search) are noun-based rather than verb-based, a minor deviation from the otherwise predictable scheme.
Five tightly scoped tools cover the invoice-analysis workflow from ingestion through extraction, search, and external enrichment. Each tool has a clear, non-redundant job, so nothing feels padded or missing at this scale.
The core lifecycle is covered: ingest, extract with validation, semantic search, plus financial and web enrichment. Minor gaps exist — no explicit list-documents or delete-document operation, and no batch or export tool, though re-ingesting replaces duplicates and search partly substitutes for listing.
Related MCP Connectors
Primary-source SEC filing intelligence and financial/disclosure reconciliation for AI agents.
Real SEC, 13F, insider, congress & macro data your AI agent can cite. Hosted MCP, 24 tools.
Private, multimodal knowledge store for agents: hybrid search with cited, provenance-backed results.
Live AI-native web search with citations. One tool for every MCP client. Flat per-request pricing.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to search, retrieve, and answer grounded questions over procurement documents (invoices, purchase orders, contracts, etc.) using hybrid SQL and vector retrieval.-
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to extract structured data from PDFs with confidence scores and provenance, and to search, review, and correct documents via MCP tools, resources, and prompts.-
- AlicenseNot gradedqualityBmaintenanceMCP server that gives AI agents read access to subscription/SaaS invoice history, ingested from Gmail and stored in Postgres and MongoDB, with tools for listing invoices, retrieving PDFs, and summarizing spend.70 npmMIT
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to parse and analyze PDF invoices, including text, field, and table extraction, OCR for scanned documents, ZUGFeRD support, compliance validation, and batch processing via MCP.-