Invoice-MCP
MCP 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.
README
Rayshio
Rayshio is an invoice aggregator software for businesses that gives your AI agents MCP access to your expenses.
How it Works
Rayshio aggregates invoices directly from a company's billing email address:
- User signs in with whatever provider their work email lives on
- Pulls a sample of invoices from the email to find the recipient email. Sometimes, billing emails are aliases that forward emails to the user's email address.
- Poll through all emails to the billed email address(es) to find all invoices
- Invoice PDF is stored in MongoDB
- Invoice data is stored in PostgreSQL
See
SPEC.mdfor the full design.
Setup
cp .env.example .envand fill in values (scripts/has key generators noted inline)docker compose up -dpnpm installpnpm migrate up— appliesmigrations/to Postgrespnpm cli seed-org --name "Your Org"pnpm cli auth— browser OAuth consent, stores encrypted refresh tokenpnpm cli check-connections— sanity check all three databases
Running
| Command | What |
|---|---|
pnpm worker |
ingestion worker (BullMQ; registers the monthly sync cron) |
pnpm mcp |
MCP server + dashboard API on :3000; also serves web/dist when built |
pnpm dev:web |
Vite dev server on :5173, proxying /api → :3000 |
pnpm cli discover |
whole-mailbox billing-sender discovery → auto-backfill |
pnpm cli backfill --service <id> |
full history for one sender |
pnpm cli sync |
incremental sync now (otherwise monthly via SYNC_CRON) |
pnpm cli categorize |
backfill usage categories onto invoice line items |
just
A root justfile wraps the common loops so nothing depends on remembering
which pnpm script maps to which process. Recipe names cannot contain a colon —
just rejects it at parse time — hence dev-web rather than dev:web.
| Command | What |
|---|---|
just |
list recipes |
just dev-mcp |
pnpm mcp — API + MCP on :3000 |
just dev-web |
pnpm dev:web — Vite on :5173 |
just dev-all |
both of the above in one terminal; Ctrl-C stops both |
just dev |
alias for dev-all |
just build |
pnpm build (backend dist/ + web/dist) |
just test |
pnpm test — full vitest run |
just clean |
pnpm lint:fix — Biome format + lint with autofix |
just kill 3000 |
kill whatever holds a port, when a dev server outlives its terminal |
dev-all runs each server in its own process group so Ctrl-C reaches the
tsx/vite child, not just the pnpm wrapper it was launched through.
Dashboard
A React SPA at /, served by the same process as the MCP server. Pages:
Dashboard (spend, budget, top vendors, recent invoices), Breakdown (usage
categories rolled up across vendors), Invoices (paginated, searchable),
Reports (the same views over fiscal quarters and years), Calendar (received +
projected), and MCP (connection recipes).
Fiscal periods derive from client.org.fiscal_year_start_month (default
January, changeable on the Reports page). Fiscal years are named for the year
they end in — an April start makes Apr 2025 – Mar 2026 into "FY2026" — and
every label carries its date range, since that convention is not universal.
Sign in with Google (Better Auth). Signing in creates a user but grants no access: membership in an org is a deliberate act, so the first person through the door does not inherit a tenant. Grant it with
pnpm cli grant-membership --org 1 --email you@example.com --role owner
Sign-up is allowlisted by ALLOWED_SIGNUP_EMAILS; an address outside it can
still join by holding a pending invitation (pnpm cli invite).
MCP keys live in client.api_key — pnpm cli create-api-key --org 1 mints one
and prints it once. No key is ever sent to the browser.
Two behaviours worth knowing:
- Currency conversion is query-time only. Invoices are stored in the currency
the vendor billed; the dashboard's currency selector is a display target, and
every invoice is converted to it at the ECB rate on that invoice's own date, so
a past month's total does not move when today's rate does. Converted values are
never written back (
SPEC.md:190-196), and converted figures are labeled with the rate date. The ECB publishes ~30 currencies — hard-pegged currencies outside that set (AED) use an explicit peg table insrc/fx/rates.ts. The MCP tools are unaffected and still sum per-currency with no conversion, which is the spec's documented default. - Categories live on line items.
billing.invoice_line_items.categoryis assigned by an LLM from a fixed five-value set, so a single vendor invoice can split across categories. Classification never fails an invoice — anything unclassified reads asOtheruntilpnpm cli categorizeretries it.
Local development runs two processes: pnpm mcp and pnpm dev:web (or
just dev-all for both), then open http://localhost:5173. In production only
pnpm start:mcp is needed, provided pnpm build has produced web/dist.
MCP tools
list_services, list_invoices, get_invoice, get_invoice_pdf,
spend_summary — all scoped to the org the presented API key belongs to; money
in minor units, per-currency aggregation, no FX conversion.
Claude Desktop's connector UI expects OAuth, which this MVP doesn't
implement — bridge with mcp-remote
in claude_desktop_config.json instead:
{
"mcpServers": {
"invoice-mcp": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://<host>/mcp",
"--header",
"Authorization: Bearer <MCP_API_KEY>"
]
}
}
}
Development
pnpm test/pnpm typecheck/pnpm lint(orjust test/just clean)pnpm codegen— regeneratesrc/db/types.tsafter schema changestsx scripts/smoke-extract.ts [pdf]— live LLM extraction smoke testtsx scripts/make-fixture-pdf.ts— regenerate the multi-page fixture PDFtsx scripts/probe-mailbox.ts [query]— read-only audit of discovery coverage: replays sender attribution over the live mailbox and lists which vendors would be found, which arrive via a billing alias, and which are already ingested
Billing aliases and shared inboxes
Vendor invoices frequently arrive through a shared billing alias or Google Group
(billing@, techteam@, …) rather than direct to the connected mailbox. The
group re-sends the mail with itself as the RFC From: address, leaving the
vendor only in the display name ("'OpenRouter, Inc' via Tech Team"). Keying on
From alone therefore collapses every vendor behind an alias into one
pseudo-sender that no classifier will accept as a vendor.
resolveSender (src/gmail/messages.ts) prefers X-Original-Sender — which
Google sets on exactly these rewrites — and records the alias as
ParsedMessage.deliveredVia. Discovery additionally refuses to treat the org's
own addresses as vendors, so invoices you forward to yourself or to a
bookkeeping tool don't get ingested a second time.
Deploy (Railway)
One repo, two services:
- worker — start
pnpm start:worker - mcp — start
pnpm start:mcp(health check/healthz); serves the MCP endpoint, the dashboard API, and the built SPA from one port
Both services are connected to this repository on main, so a push to main
deploys them. railway up still works and deploys the working tree instead,
which is the escape hatch for testing a change that is not committed — but it
is no longer how a release happens.
Both build with pnpm install && pnpm build (which also builds web/dist).
Migrations run automatically at boot for both roles — the web service can
otherwise start before the worker has applied one and serve sign-ins against a
missing table — so no pre-deploy command is needed. A simultaneous deploy is
safe: node-pg-migrate locks with pg_try_advisory_lock, which does not wait, so
the loser is retried by src/main.ts rather than crashing.
Set env vars per .env.example (Railway injects database URLs for linked
Postgres/Mongo/Redis services). Both services need BETTER_AUTH_SECRET,
AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, PUBLIC_APP_URL and
ALLOWED_SIGNUP_EMAILS; the mcp service additionally needs PUBLIC_MCP_URL
set to its public https://<host>/mcp. Build the SPA with
VITE_PUBLIC_ORIGIN=https://<host> so canonical, OG and sitemap URLs are
absolute and correct.
Sign-in uses a separate Google OAuth client from Gmail ingestion. The
ingestion client carries gmail.readonly, a restricted scope, so sharing it
would show a mailbox-access consent screen for a plain login. Register
https://<host>/api/auth/callback/google on the sign-in client.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。