Invoice-MCP-generator
Provides a tool for moving files within Google Drive.
Provides tools for interacting with Google Sheets, including reading ranges, updating cells, appending rows, and listing tabs.
Provides a tool for sending WhatsApp messages (currently inactive until Meta approval).
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., "@Invoice-MCP-generatorGenerate invoice for order 1001 and upload the PDF to Google Drive"
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.
Invoice Generator — LHG Import Export Hub
Phase 2 of the WhatsApp order automation project: structured order → HTML → PDF.
Current status
✅ Phase 1: Product catalog (
catalog.json) — done. 28 items loaded, only Royal Lion Potato Starch has full pricing; everything else is flaggedneeds_reviewuntil the Excel is filled in and re-converted.✅ Phase 2: Invoice generator — done. Products are matched by free-text query (e.g. "button mushroom"), not barcode — barcode is an optional internal shortcut. Ambiguous or unmatched queries throw a clear error with candidates instead of guessing.
✅ Phase 3: Google Drive upload — done. PDFs auto-upload to
Invoices/{year}/{month}/in Google Drive, auto-creating subfolders.⬜ Phase 4: WhatsApp webhook — not started.
⬜ Phase 5: AI order parsing — not started.
Related MCP server: gworkspace-mcp
Setup
npm install
cp .env.example .envFill in .env with your Supabase project URL and service key, and your
Google Drive service account key path + folder ID.
For Google Drive (OAuth):
In Google Cloud Console → APIs & Services → Credentials → Create Credentials → OAuth client ID → type "Desktop app".
Copy the Client ID and Client Secret into
.env.Run
node get-refresh-token.jsonce — it prints a URL, you approve access in your browser, paste back a code, and it prints yourGOOGLE_OAUTH_REFRESH_TOKEN. Copy that into.env.Share your Drive "Invoices" root folder with the Google account you authorized in step 3 (or just use that account's own Drive directly).
Copy that folder's ID from its URL into
GOOGLE_DRIVE_FOLDER_ID.
You also need to run this SQL once in your Supabase project (SQL editor):
create table invoice_counters (
year int not null,
month int not null,
last_number int not null default 0,
primary key (year, month)
);
create or replace function get_next_invoice_number(p_year int, p_month int)
returns int as $$
declare
next_num int;
begin
insert into invoice_counters (year, month, last_number)
values (p_year, p_month, 1)
on conflict (year, month)
do update set last_number = invoice_counters.last_number + 1
returning last_number into next_num;
return next_num;
end;
$$ language plpgsql;Usage
const { generateInvoice } = require('./generate-invoice');
const order = {
customer_name: 'ABC Restaurant',
customer_address: '123 Jalan Test, Kota Kinabalu, Sabah',
customer_phone: '+60 12-345 6789',
payment_method: 'Bank Transfer',
items: [
{ barcode: '9551029670183', quantity: 2 } // barcode must exist + be priced in catalog.json
],
};
generateInvoice(order).then((result) => {
console.log(result.invoiceNumber, result.outputPath);
});Or run the built-in test:
npm run generateNote: the test order in generate-invoice.js currently has an empty items
list, because Royal Lion Potato Starch (the only fully-priced item) is
missing its barcode in the Excel. Running it as-is will fail on purpose
with "Order has no items" — that's the fail-safe behavior working correctly,
not a bug. Fill in a barcode + quantity once at least one product is fully
priced with a barcode, to see a real PDF generated.
How pricing works
Tier is selected automatically from quantity ordered:
Quantity | Tier |
1–3 |
|
4–10 |
|
11–20 |
|
21–49 |
|
50+ |
|
If the matched tier has no price in catalog.json, or the barcode doesn't
exist, generation stops and throws — it never guesses or generates a
partial invoice.
Updating the catalog
Edit the master Excel.
Send it back through Claude to regenerate
catalog.json.Replace
catalog.jsonin this project.
Folder structure
invoice-generator/
├── generate-invoice.js # main entry point (plain Node, unrelated to Next.js/Vercel)
├── invoice-template.html # FIXED invoice design — edit freely, placeholders must stay
├── catalog.json # product data — regenerate from Excel, don't hand-edit
├── next.config.js # minimal Next.js config
├── src/
│ ├── catalog.js # catalog loading + lookup
│ ├── pricing.js # tier selection + line/total calculations
│ ├── counter.js # Supabase-backed running invoice number
│ ├── render.js # fills HTML template with order data
│ ├── pdf.js # HTML → PDF via Puppeteer
│ ├── auth.js # shared Google OAuth client (Drive + Sheets, server-side refresh flow)
│ ├── drive.js # invoice PDF upload to Drive
│ ├── sheetsTools.js # MCP tools: read/write/append Google Sheets cells
│ ├── mcpDriveTools.js # MCP tools: move files in Drive
│ └── whatsappTools.js # MCP tool: send WhatsApp message (inactive until Meta approval)
├── lib/
│ ├── jwt.js # sign/verify all tokens this app issues (auth codes, access, refresh, relay state)
│ ├── pkce.js # PKCE S256 verification
│ └── google-identity.js # Google login used ONLY for identity check inside /api/authorize
├── app/
│ ├── api/
│ │ ├── mcp/route.js # the 6 MCP tools, protected by withMcpAuth
│ │ ├── authorize/route.js # OAuth /authorize — hands off to Google for identity
│ │ ├── oauth/google/callback/route.js # Google redirects back here; mints our own auth code
│ │ └── token/route.js # OAuth /token — issues our own access/refresh tokens
│ └── .well-known/
│ ├── oauth-protected-resource/route.js # RFC 9728
│ └── oauth-authorization-server/route.js # RFC 8414
├── output/ # generated PDFs land here (gitignored)
└── .env # secrets — never committedMCP server — real OAuth 2.1, not a shared secret
This app is deployed as a Next.js project on Vercel and does two unrelated jobs from the same codebase:
Resource Server — exposes 6 tools (
sheets_read_range,sheets_update_range,sheets_append_row,sheets_list_tabs,drive_move_file,whatsapp_send_message) at/api/mcp, usingmcp-handlerv2 +@modelcontextprotocol/serverv2 (Streamable HTTP).Authorization Server — issues the tokens Claude uses to call those tools, at
/api/authorizeand/api/token. Google is used inside this flow only to confirm identity — Google's own access token never reaches Claude.
Why not just use Google directly as the Authorization Server?
Claude requires every access token's aud claim to exactly match this
server's canonical URL (RFC 8707 — Claude enforces this as a MUST). Google's
OAuth server doesn't support resource-scoped tokens — it always audiences
tokens to its own Client ID, never to an arbitrary URL. Passing a raw
Google token to Claude would fail Claude's audience check outright. So this
app mints its own resource-bound tokens instead, using Google only as a
one-time login check inside /api/authorize.
The flow
Claude → GET /api/mcp (no token)
← 401 + WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource"
Claude → GET /.well-known/oauth-protected-resource
← { resource: ".../api/mcp", authorization_servers: [".../"] }
Claude → GET /.well-known/oauth-authorization-server (on OUR domain, not Google's)
← { authorization_endpoint: ".../api/authorize", token_endpoint: ".../api/token", ... }
Claude → browser opens GET /api/authorize?client_id=...&code_challenge=...&resource=...
→ we redirect to Google's real login screen (identity check only)
← Google redirects back to /api/oauth/google/callback with a Google code
→ we exchange it, read the email, check it against ALLOWED_GOOGLE_EMAIL
→ we mint OUR OWN short-lived authorization code, redirect back to Claude
Claude → POST /api/token (grant_type=authorization_code, our code + PKCE verifier)
← { access_token: <our JWT, aud=.../api/mcp>, refresh_token: <our JWT> }
Claude → POST /api/mcp Authorization: Bearer <our access token>
→ withMcpAuth verifies signature + issuer + audience + scope
→ tool runs exactly as beforeTwo Google OAuth interactions exist in this codebase and never touch each
other: src/auth.js (server's own long-lived Drive/Sheets access, used by
both the invoice generator and the MCP tools) and
lib/google-identity.js (one-time human login check inside /api/authorize
only). Same Google Cloud OAuth Client, two separate purposes.
Environment variables
All secrets live in Vercel's environment variables — nothing is hardcoded
in source. See .env.example for the full annotated list. Summary:
Variable | Purpose |
| Your app's stable production URL — used to construct every issuer/resource/redirect value consistently |
| Signs every token this app issues. Rotating it revokes everything at once (this design has no per-token revocation) |
| Values you make up, entered into Claude's "Use your own OAuth client" field |
| The one Google account allowed to complete login |
| Reused from the existing Drive/Sheets setup — needs one new redirect URI added (see below) |
| Unchanged from before |
Google Cloud Console setup
Add both of these to your existing OAuth Client's Authorized redirect URIs
(alongside whatever get-refresh-token.js already uses):
{MCP_ISSUER_URL}/api/oauth/google/callback
http://localhost:3000/api/oauth/google/callback (for local testing)Claude custom connector setup
In Claude's connector dialog → Advanced settings → "Use your own OAuth client":
Server URL:
{MCP_ISSUER_URL}/api/mcpClient ID: your
MCP_CLIENT_IDvalueClient Secret: your
MCP_CLIENT_SECRETvalue
Claude's own redirect URIs (already whitelisted on Anthropic's side, nothing
for you to configure): https://claude.ai/api/mcp/auth_callback and
https://claude.com/api/mcp/auth_callback.
Deploy
npm install
npm install -g vercel
vercel # first deploy, get your URL, set MCP_ISSUER_URL to it
vercel --prod # redeploy after setting all env vars in the dashboardLocal test procedure
Run npm run dev (starts on http://localhost:3000) with .env filled in
and MCP_ISSUER_URL=http://localhost:3000.
1. OAuth discovery
curl http://localhost:3000/.well-known/oauth-protected-resource
curl http://localhost:3000/.well-known/oauth-authorization-serverBoth should return JSON (not 404). Confirm resource and issuer show
http://localhost:3000.
2. Authorization (needs a real browser — this step logs you into Google) Open in a browser:
http://localhost:3000/api/authorize?response_type=code&client_id=YOUR_MCP_CLIENT_ID&redirect_uri=http://localhost:3000/callback-test&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=http://localhost:3000/api/mcp(The code_challenge above corresponds to verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk — a fixed PKCE test pair, fine for local testing.)
You should be sent to Google's real login screen, then redirected back to
http://localhost:3000/callback-test?code=... (that URL will 404 since
there's no real page there — that's fine, just copy the code value from
the address bar).
3. Token issuance
curl -X POST http://localhost:3000/api/token \
-d "grant_type=authorization_code" \
-d "code=PASTE_CODE_FROM_STEP_2" \
-d "redirect_uri=http://localhost:3000/callback-test" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
-d "client_id=YOUR_MCP_CLIENT_ID" \
-d "client_secret=YOUR_MCP_CLIENT_SECRET"Should return access_token, refresh_token, expires_in: 3600.
4. MCP authentication
# Without a token - should get 401 with WWW-Authenticate header
curl -i -X POST http://localhost:3000/api/mcp -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}'
# With the access token from step 3 - should succeed
curl -X POST http://localhost:3000/api/mcp \
-H "Authorization: Bearer PASTE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}'5. Calling each tool — list them first, then call one as a smoke test:
curl -X POST http://localhost:3000/api/mcp \
-H "Authorization: Bearer PASTE_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2,"params":{}}'
curl -X POST http://localhost:3000/api/mcp \
-H "Authorization: Bearer PASTE_ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"sheets_list_tabs","arguments":{"spreadsheetId":"YOUR_TEST_SHEET_ID"}}}'Repeat for sheets_read_range, sheets_update_range, sheets_append_row,
drive_move_file, and whatsapp_send_message (the last will return its
"not yet configured" message until WHATSAPP_TOKEN is set — that's expected).
This server cannot be deployed
Maintenance
Related MCP Connectors
PDF tools + invoice extraction, bank statement parsing, GST reconciliation & GSTIN validation.
Invoicing you drive by talking to your AI: log time, raise invoices and track what's owed via MCP.
Generate professional receipt PDFs and structured receipt data with AI-ready MCP tools.
Send invoices from Claude and ChatGPT. One sentence becomes a PDF with a Stripe payment link.
Related MCP Servers
- FlicenseAqualityCmaintenanceYour AI Assistant's Gateway to Google Sheets! 25 powerful tools for seamless Google Sheets automation via MCP2444 PyPI13-
- AlicenseBqualityDmaintenanceAn MCP server for automating Google Workspace applications including Sheets, Apps Script, Drive, Docs, and Gmail. It enables users to manipulate spreadsheets, edit scripts, manage files, and send emails directly from conversational AI interfaces.31MIT
- AlicenseNot gradedqualityBmaintenanceMCP server for Google Drive, Docs, and Sheets — built for Claude Code. Gives Claude Code direct read/write access to Google Sheets (cell-level edits, formatting, structure), Google Docs (insert, replace, append), and Drive (search).35 npm1MIT
- AlicenseNot gradedqualityDmaintenanceA comprehensive MCP server integrating Google Docs, Sheets, Drive, Gmail, Calendar, and Apps Script APIs, featuring unique Apps Script support for AI-driven automation.6 npm8MIT