MCP server for Factus — Colombian electronic invoicing platform (facturación electrónica DIAN). Exposes eight tools over HTTP, deployable via Docker and GitHub Actions.
| Layer | Technology |
|---|---|
| Runtime | Node.js 20 (ESM) |
| Language | TypeScript 5 — strict, NodeNext module resolution |
| MCP protocol | @modelcontextprotocol/sdk — Streamable HTTP transport (stateless) |
| Validation | Zod |
| Package manager | pnpm |
| Containerization | Docker — multi-stage build, non-root user, read-only FS |
| Reverse proxy | Nginx — TLS termination, rate limiting (60 req/min per IP) |
| CI/CD | GitHub Actions — deploy on version tag push (v*) |
src/
index.ts — entry point: wires transport, MCP server, HTTP server
config.ts — Zod env validation
factus/
auth.ts — OAuth2 token manager: get + refresh + dedup concurrent calls
client.ts — factusGet / factusPost / factusDelete
types.ts — TypeScript interfaces for Factus API responses
tools/
types.ts — ToolDefinition interface
index.ts — tool registry (add a tool = add one file + one line here)
create-invoice.ts
get-invoice.ts
list-invoices.ts
create-credit-note.ts
list-numbering-ranges.ts
lookup-customer.ts
send-invoice-email.ts
get-subscription.ts
server/
mcp.ts — MCP server: tool list + dispatch
http.ts — HTTP server: /health + /mcp routing
Factus uses OAuth2 password grant. Tokens expire every 10 minutes. src/factus/auth.ts manages this automatically:
- First call triggers
POST /oauth/tokenwith credentials. - Token is cached in memory with its expiry.
- Subsequent calls reuse the cached token until 30 seconds before expiry.
- On expiry, a refresh is attempted with the
refresh_token. If that fails, a full re-auth runs. - Concurrent requests during a refresh share the same pending promise (no thundering herd).
No credentials are stored on disk — only in process memory.
The server speaks MCP over Streamable HTTP (stateless mode):
POST /mcp— client sends JSON-RPC tool requestsGET /mcp— SSE stream for server-sent eventsDELETE /mcp— session terminationGET /health— returns{ "status": "ok", "env": "..." }
| Tool | Description |
|---|---|
list_numbering_ranges |
List active numbering ranges — get numbering_range_id before invoicing |
lookup_customer |
Look up customer name/email from DIAN by NIT or cédula |
create_invoice |
Create and validate an electronic invoice with DIAN |
get_invoice |
Fetch invoice details by number |
list_invoices |
List invoices with filters (customer, status, date) |
create_credit_note |
Create a credit note (nota crédito) against an existing invoice |
send_invoice_email |
Resend an invoice to any email address |
get_subscription |
Check plan status and remaining document quota |
1. lookup_customer(nit) → auto-fill names, email
2. list_numbering_ranges() → pick numbering_range_id
3. create_invoice(...) → get number + CUFE
4. send_invoice_email(number, email)
| Code | Type |
|---|---|
13 |
Cédula de ciudadanía |
31 |
NIT |
22 |
Cédula de extranjería |
| Code | Unit |
|---|---|
70 |
Unidad |
94 |
Servicio |
120 |
Hora |
| Code | Tax | Common rates |
|---|---|---|
01 |
IVA | 19%, 5%, 0% |
04 |
INC | varies |
03 |
ICA | varies |
| Code | City |
|---|---|
11001 |
Bogotá |
05001 |
Medellín |
76001 |
Cali |
08001 |
Barranquilla |
13001 |
Cartagena |
- Create an account at factus.com.co.
- Go to Configuración > API to get
client_idandclient_secret. - Your
usernameis the email you registered with. - Your
passwordis your Factus account password. - For sandbox testing, use app-sandbox.factus.com.co.
cp .env.example .env
# fill in your sandbox credentials
pnpm install
pnpm dev # tsx --watch, no build step neededServer starts on http://localhost:7779.
{
"mcpServers": {
"factus": {
"type": "http",
"url": "http://localhost:7779/mcp"
}
}
}Or, once deployed:
{
"mcpServers": {
"factus": {
"type": "http",
"url": "https://factus-mcp.danigomez.dev/mcp"
}
}
}cp .env.example .env.local
# fill in your credentials
docker compose up --build -dContainer binds to 127.0.0.1:7779 only. Read-only filesystem, all Linux capabilities dropped, limited to 256 MB RAM / 0.5 CPU.
Triggers automatically on git tag v* && git push --tags.
| Secret | Description |
|---|---|
SERVER_KEY_SSH |
Base64-encoded SSH private key |
SERVER_HOST |
Server IP or hostname |
SERVER_USER |
SSH username |
GIT_TOKEN |
GitHub PAT with repo read access |
GIT_USER |
GitHub username |
GIT_REPO |
Repository name (e.g. org/mcp.factus) |
FACTUS_CLIENT_ID |
OAuth2 client ID (production) |
FACTUS_CLIENT_SECRET |
OAuth2 client secret (production) |
FACTUS_USERNAME |
Factus account email (production) |
FACTUS_PASSWORD |
Factus account password (production) |
CLOUDFLARE_ORIGIN_CERT |
Cloudflare origin certificate (PEM) |
CLOUDFLARE_ORIGIN_KEY |
Cloudflare origin private key |
Two places to change in .github/workflows/nginx.yml:
- Replace every occurrence of
factus-mcp.danigomez.devwith your domain. - Rename the cert file variables (
danigomez.dev.pem/danigomez.dev.key) to match your domain.
git tag v1.0.0
git push origin v1.0.0- Nginx: 60 req/min per IP (burst 15) — protects the server
- Factus API: 80 req/min per account — see
X-RateLimit-Remainingheader
| Command | Description |
|---|---|
pnpm dev |
Run with tsx watch (no build needed) |
pnpm build |
Compile TypeScript to dist/ |
pnpm start |
Run compiled output |
pnpm lint |
ESLint |
pnpm typecheck |
Type check without emitting |