Skip to content

tulkyorg/mcp.factus

Repository files navigation

mcp.factus

MCP server for Factus — Colombian electronic invoicing platform (facturación electrónica DIAN). Exposes eight tools over HTTP, deployable via Docker and GitHub Actions.


Tech stack

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*)

Architecture

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

Authentication

Factus uses OAuth2 password grant. Tokens expire every 10 minutes. src/factus/auth.ts manages this automatically:

  • First call triggers POST /oauth/token with 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.

Protocol

The server speaks MCP over Streamable HTTP (stateless mode):

  • POST /mcp — client sends JSON-RPC tool requests
  • GET /mcp — SSE stream for server-sent events
  • DELETE /mcp — session termination
  • GET /health — returns { "status": "ok", "env": "..." }

Available tools

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

Typical agent workflow

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)

DIAN reference codes (most common)

Identification document

Code Type
13 Cédula de ciudadanía
31 NIT
22 Cédula de extranjería

Unit measures

Code Unit
70 Unidad
94 Servicio
120 Hora

Tax codes

Code Tax Common rates
01 IVA 19%, 5%, 0%
04 INC varies
03 ICA varies

Municipality codes (main cities)

Code City
11001 Bogotá
05001 Medellín
76001 Cali
08001 Barranquilla
13001 Cartagena

Getting your Factus credentials

  1. Create an account at factus.com.co.
  2. Go to Configuración > API to get client_id and client_secret.
  3. Your username is the email you registered with.
  4. Your password is your Factus account password.
  5. For sandbox testing, use app-sandbox.factus.com.co.

Local development

cp .env.example .env
# fill in your sandbox credentials

pnpm install
pnpm dev        # tsx --watch, no build step needed

Server starts on http://localhost:7779.


Connecting to Claude Code

{
  "mcpServers": {
    "factus": {
      "type": "http",
      "url": "http://localhost:7779/mcp"
    }
  }
}

Or, once deployed:

{
  "mcpServers": {
    "factus": {
      "type": "http",
      "url": "https://factus-mcp.danigomez.dev/mcp"
    }
  }
}

Docker

cp .env.example .env.local
# fill in your credentials

docker compose up --build -d

Container binds to 127.0.0.1:7779 only. Read-only filesystem, all Linux capabilities dropped, limited to 256 MB RAM / 0.5 CPU.


Production deployment (GitHub Actions)

Triggers automatically on git tag v* && git push --tags.

Required GitHub secrets

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

Deploying to a different domain

Two places to change in .github/workflows/nginx.yml:

  1. Replace every occurrence of factus-mcp.danigomez.dev with your domain.
  2. Rename the cert file variables (danigomez.dev.pem / danigomez.dev.key) to match your domain.

Release

git tag v1.0.0
git push origin v1.0.0

Rate limits

  • Nginx: 60 req/min per IP (burst 15) — protects the server
  • Factus API: 80 req/min per account — see X-RateLimit-Remaining header

Scripts

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

Releases

Packages

Contributors

Languages