Live demo — data resets daily at 03:00 UTC. Nothing you enter is saved.
Fortnox MCP Integration
FORTNOX-MCP · Rev R0
softwarein_progress
Generated 2026-07-26 13:59 UTC

Integrating an MCP server for the Fortnox Swedish accounting system so LLMs can manage invoices, customers, suppliers, orders, vouchers, and accounting data. Evaluating and standing up an existing open-source Fortnox MCP server as the base.

Coverage 0%
Done 0%
Requirements 0
Test Cases 0
Records 0
Blockers 1

Milestones

No milestones defined.

Items (11)

Code Status Severity Kind Title Date
E-006 open blocking issue Company 2 cannot be integrated — no API/integration license 2026-06-06
E-003 open warning issue Leaked hardcoded credentials in get-token.ts (v1.0.1) 2026-06-06
E-001 open info decision Use erp-mafia/fortnox-mcp as the base MCP server 2026-06-06
E-002 open info decision Run server from built source on main, not npx/v1.0.1 2026-06-06
E-005 open info decision Local patch: FORTNOX_TOKEN_FILE for per-instance token caches (arbitrary companies) 2026-06-06
E-007 open info notice get-token.ts local fixes + E# wired up (7 scopes, connected) 2026-06-06
E-008 open info decision Hardened the MCP server: fixed 4 data-integrity/reliability bugs, contributed upstream via PR #6 2026-06-06
E-009 open info decision Deployment architecture: shared-account Docker server on LAN behind a bearer-token reverse proxy 2026-06-06
E-010 open info notice Deployment LIVE on factory.local:9000 — Docker stack verified end-to-end 2026-06-06
E-011 open info notice How to get access: connecting to the shared Fortnox MCP server 2026-06-06
E-004 closed warning notice Server built & smoke-tested; deps patched (0 prod vulns) 2026-06-06

Requirements

No requirements recorded.

Test Cases

No test cases recorded.

Verification Records

No verification records.

Issues

E-006 Company 2 cannot be integrated — no API/integration license blocking open

Problem

Company 2's OAuth authorization fails with error_missing_license for every scope set tried, down to companyinformation alone. Tested (2026-06-06), all failed:

  • 7 scopes (full)
  • 5 core (customer, invoice, supplier, bookkeeping, companyinformation)
  • bookkeeping companyinformation
  • companyinformation only

Making the user a system administrator on company 2 did not change the result — so it is not a user-permission issue.

Diagnosis

error_missing_license even for the most basic scope means company 2's Fortnox subscription does not include external API / integration access (most likely a free/limited tier such as free Bokföring). This is a plan/licensing matter on the Fortnox side — not fixable in code or config.

Resolution path (needs user / Fortnox)

  • Upgrade company 2 to a Fortnox plan that includes integrations / API access (or add the integration add-on), then re-run the OAuth flow.
  • Until then, company 2 stays unwired. Company E# is unaffected and fully authorized.

Status

Blocking for company 2 only. Recommend confirming company 2's plan in Fortnox (admin → subscription) before any upgrade.

E-003 Leaked hardcoded credentials in get-token.ts (v1.0.1) warning open

Finding (security vetting of base repo)

At tag v1.0.1, scripts/get-token.ts lines 20–21 hardcode the maintainer's real Fortnox client_id and client_secret as fallback defaults:

const CLIENT_ID = process.env.FORTNOX_CLIENT_ID || "A3fjvEGS3mUw";
const CLIENT_SECRET = process.env.FORTNOX_CLIENT_SECRET || "<real secret committed in repo>";

Impact: running the helper without first exporting your own credentials would mint a refresh token against the maintainer's Fortnox app, intermediating your accounting-data access through their app identity. Also a hygiene red flag (a real secret is committed to a public repo).

Scope limit: the leaked secret lives only in scripts/, which is NOT in the published npm package — the running server (src/auth/credentials.ts) has no embedded secret and requires env vars. So this only affected the one-time token-acquisition step.

Resolution

Fixed upstream after v1.0.1 by the 2026-04-28 commit "Require FORTNOX_CLIENT_ID and FORTNOX_CLIENT_SECRET env vars in get-token.ts". We adopt main, which includes the fix, so no local patch needed [[run-server-from-built-source-on-main]]. Closed.

Rest of auth vetting (clean)

Standard OAuth2 against official apps.fortnox.se/oauth-v1 endpoints; client secret only sent as Basic auth to Fortnox; refresh-token rotation honored; no telemetry/exfiltration. Minor platform note: token file ~/.fortnox-mcp/tokens.json uses 0600/0700 modes which are effectively no-ops on Windows NTFS — protect the user profile instead.

Decisions

E-001 Use erp-mafia/fortnox-mcp as the base MCP server info

Decision

Adopt erp-mafia/fortnox-mcp as the base Fortnox MCP server, rather than building from scratch or using the alternative.

Rationale

Evaluated two existing open-source options (data as of 2026-06-06):

erp-mafia/fortnox-mcp (chosen) Proviscale/fortnox-mcp-server (rejected)
Stars / forks 30 / 14 1 / 0
Commit history Real history + tagged releases (v1.0.1) 1 commit only ("Initial release")
Last code push 2026-04-28 2026-04-01 (none since)
Tools/endpoints 40+ across 10 categories incl. BI/analytics 20 across 6 categories
Language TypeScript (94%) JavaScript (69%)
License MIT None specified (all rights reserved)
Auth OAuth2 local (env) + remote (browser OAuth, JWT, Redis) OAuth2 access token only
Transports stdio + HTTP basic
Distribution npm + MCP Registry source only

Decisive factors: active maintenance with real releases, ~2x endpoint coverage (incl. BI/analytics), TypeScript, both stdio + HTTP transports, and published to npm/MCP Registry. The alternative is a single-commit prototype.

Rejected alternatives

  • Proviscale/fortnox-mcp-server — rejected: single-commit prototype, no maintenance since initial commit, half the endpoint coverage, and crucially no license (legally all-rights-reserved, so no permission to use/modify/redistribute) — close to disqualifying for business use of accounting data.
  • Build from scratch — rejected for now: erp-mafia already covers the needed surface; revisit only if vetting reveals blocking issues.

Open follow-ups (not blocking this decision)

  • Single-maintainer (~30 stars) project — vet source, especially OAuth/token-storage handling, before pointing at production Fortnox data.
  • Pin a version rather than tracking latest.
  • Trust check on author (Jakob Wennberg) since the server holds the Fortnox client secret + refresh token.
  • Test against a non-production Fortnox company before exposing write-tools (create voucher/invoice).
E-002 Run server from built source on main, not npx/v1.0.1 info

Decision

Run the Fortnox MCP server from locally built source on the main branch, rather than via npx -y fortnox-mcp-server (which pulls the npm-published v1.0.1).

Context / rationale

  • npx -y fortnox-mcp-server always runs the npm-published version (v1.0.1) regardless of the git checkout — the clone only feeds the get-token.ts helper. So "running main" requires building from source and pointing the MCP client config at the built dist/index.js.
  • main is only 2 commits ahead of v1.0.1 (both 2026-04-28): (1) get-token.ts now requires env-var credentials [[leaked-credentials-in-get-token-v1-0-1]], (2) remote-mode OAuth callback returns plain-text errors (irrelevant to local mode).
  • Running from vetted source means we run exactly the code we audited, not an opaque npm tarball trusted to match the tag — stronger for a single-maintainer dependency handling accounting credentials.
  • The get-token footgun is already fixed on main, so no local patch is needed.

Tradeoffs accepted

  • Requires a build step (npm install + npm run build) and re-pull/rebuild to update.
  • MCP client config points at an absolute path to dist/index.js instead of the simpler npx command.
  • main is a moving target (not a pinned release); revisit if we want reproducibility.

Rejected alternative

  • npx + v1.0.1 — simpler, zero build, but runs an opaque published tarball and we'd miss the get-token fix; superseded by the security-hygiene argument above.

Repo cloned at C:\tmp\claude\fortnox\fortnox-mcp, currently on main (501ee13).

E-005 Local patch: FORTNOX_TOKEN_FILE for per-instance token caches (arbitrary companies) info

Decision

Apply a small local patch to src/auth/fileTokenStore.ts so the token cache path is configurable via a new FORTNOX_TOKEN_FILE env var, defaulting to the original ~/.fortnox-mcp/tokens.json when unset.

Why

Stock behavior hardcodes the cache to a single file and prefers it over the env-var refresh token (envVarProvider.ts:32-39). Running multiple server instances (one per Fortnox company) would clobber each other's tokens. This patch gives each instance its own cache → supports an arbitrary number of companies, not just two. One shared Client ID/Secret (the app), one refresh token + one token file + one MCP entry per company.

Change

// path import: add dirname
const TOKEN_FILE = process.env.FORTNOX_TOKEN_FILE
  ? process.env.FORTNOX_TOKEN_FILE
  : join(homedir(), ".fortnox-mcp", "tokens.json");
const TOKEN_DIR = dirname(TOKEN_FILE);

Verification

Rebuilt; confirmed via the built module: with FORTNOX_TOKEN_FILE set it reads that file; unset it returns to the default path. Default behavior unchanged.

Maintenance note

This is a local diff on main, not upstreamed — it will be lost on git pull/reset. Re-apply (or upstream as a PR) after updating the base. Consider contributing it back to erp-mafia. [[run-server-from-built-source-on-main]]

E-008 Hardened the MCP server: fixed 4 data-integrity/reliability bugs, contributed upstream via PR #6 info

Status

The base server (erp-mafia/fortnox-mcp) is stood up and running locally against the live E-Sharp Fortnox account. While exercising it for real financial analyses (cash-flow forecast, payables report, full-year ledger P&L) we found and fixed four bugs, all of which were silently producing wrong or missing numbers.

Fixes (all on branch fix/mcp-reliability)

  1. Supplier-invoice amounts concatenated instead of summed. Fortnox returns supplier-invoice Balance/Total as strings; sum + value concatenated them (cash-flow forecast reported a net position of -8.9e+47). Coerced to numbers in fortnox_cash_flow_forecast and fortnox_payables_report.
  2. fortnox_account_activity published an empty MCP input schema because its Zod schema ended in .refine() (ZodEffects has no .shape). Clients sent all args as strings; every call was rejected. Removed the top-level refine; validation moved to the handler.
  3. Loud completeness reporting for account_activity: raised scan cap 500→2000, added failed_detail_fetches + summary_complete + an explicit truncation/drop warning. Previously a rate-limited scan silently dropped ~95% of rows while reporting success.
  4. Resilient HTTP layer (api.ts): added 429/5xx retry with backoff honoring Retry-After, and serialized the rate limiter with ~210ms spacing so concurrent calls form a steady ~5 req/s stream instead of an instant 25-request burst.

Verification

A one-month account_activity scan went from 253/275 failed detail fetches (7 matches) to 0 failures (43 matches). End-to-end, a complete 12-month FY5 ledger P&L ran (~3,000 sequential fetches, 765s) with zero drops and every month summary_complete: true; revenue reconciled against billed invoices.

Contribution path

daniel-rhodin has no direct push access to erp-mafia/fortnox-mcp, so the change was pushed to the fork daniel-rhodin/fortnox-mcp and a PR opened upstream: https://github.com/erp-mafia/fortnox-mcp/pull/6 (awaiting review/merge).

Next

  • Merge PR #6.
  • Candidate follow-up: a dedicated fortnox_profit_and_loss tool baking in month-chunking + BAS-class bucketing (70–76 personnel / 78 depreciation / 79 other) for one-call annual P&L.
E-009 Deployment architecture: shared-account Docker server on LAN behind a bearer-token reverse proxy info

Decision

Run the Fortnox MCP server as a Docker deployment on an internal host so colleagues can share it, configured as: single shared Fortnox account · LAN-only · shared bearer token via reverse proxy · all tools exposed.

Rationale (decisions + why)

  • Single shared account (local/HTTP mode), not per-user OAuth. Everyone works on the same company books (E-Sharp AB), so one service authorization is the natural fit. Per-user OAuth (remote mode) would add a Redis token store, a public HTTPS URL, and Fortnox callback config for no benefit when there's only one company. Server runs TRANSPORT=http with one credential set.
  • LAN-only reachability. Keeps the financial endpoint off the public internet entirely — simplest and lowest attack surface. Colleagues are in the office network.
  • Shared bearer token via a Caddy reverse proxy. Local/HTTP mode has no built-in auth, and the toolset can read and modify the books, so an unauthenticated port on the LAN = anyone can rewrite the accounts. Caddy gates /mcp on an Authorization: Bearer token; only Caddy publishes a port (8080), the MCP server (3000) stays internal. Chosen over per-user OAuth because it's one secret to distribute for a single-account setup.
  • All tools exposed (incl. write/delete/bookkeep). Colleagues need full capability, not just read. Accepted risk is mitigated by the bearer token + LAN isolation.

Key operational caveats (captured so they aren't rediscovered painfully)

  • Dedicated refresh token required. Fortnox rotates the refresh token on every refresh; two instances sharing one token log each other out. The server must get its OWN authorization (run scripts/get-token.ts once) — do not reuse the token a local Claude instance uses.
  • Token persistence is mandatory. The rotating refresh token is persisted to /data/tokens.json; this is a Docker named volume (fortnox-tokens). Deleting the volume forces re-authorization.
  • Plain HTTP on the LAN means the bearer token is sniffable by someone already on the wire — acceptable on a trusted LAN; upgrade Caddy to TLS (tls internal or a real cert) if needed.
  • No per-user audit in shared mode — all actions hit Fortnox as the one service identity. Switch to remote/OAuth mode if per-user accountability is ever required.
  • The global rate limiter is process-wide, so all colleagues' combined traffic is paced under Fortnox's 5 req/s automatically.

Artifacts added to the repo

Dockerfile (multi-stage), docker-compose.yml (mcp + caddy), Caddyfile (bearer gate), .env.example, .dockerignore, DEPLOYMENT.md (full guide incl. client config for Claude Code/Desktop). docker compose config validates; full build/run pending on the target Linux host (Docker daemon not running on the dev box used to author this).

Builds on the reliability fixes in [[E-008]] / PR #6 — the deployed image should include that branch.

Notices

E-004 Server built & smoke-tested; deps patched (0 prod vulns) warning closed

Build state (2026-06-06)

  • Node.js 24.16.0 LTS installed via winget (OpenJS.NodeJS.LTS); npm 11.13.0. Was not previously installed.
  • Repo cloned at C:\tmp\claude\fortnox\fortnox-mcp, on main (501ee13).
  • npm cinpm audit fix (non-breaking) → npm run build ✅. dist/index.js builds.
  • Smoke tests pass: errors cleanly with no credentials; boots and holds on stdio with dummy credentials (all 40+ tools register).

Dependency security

  • After npm audit fix: production-only audit = 0 vulnerabilities (axios, @modelcontextprotocol/sdk, express/qs/path-to-regexp all patched within semver).
  • 9 remaining vulns (3 moderate, 6 high) are confined to @vercel/* build/deploy deps (Vercel remote-mode path we don't use). Fixing requires npm audit fix --force → breaking @vercel/node@4 downgrade. Deferred as not-applicable to local stdio runtime.

⚠️ Risk to address

  • The server runs from source at C:\tmp\claude\... — a temp path that may be cleared by cleanup/reboot, which would break the MCP server. Action: relocate the repo to a stable location (e.g. under the user profile or a projects dir) before wiring up the MCP client config, and point the config at that stable path.

Remaining (needs user — requires Fortnox login)

  1. Create Fortnox developer app; register redirect URI http://localhost:8888/callback.
  2. Run npx tsx scripts/get-token.ts with own CLIENT_ID/SECRET to get refresh token.
  3. Register MCP server in client pointing at node <stable-path>\dist\index.js with the 3 env vars.
  4. Test read-only against a non-production Fortnox company first. Note: get-token scopes omit order — order tools may fail until added [[leaked-credentials-in-get-token-v1-0-1]].
E-007 get-token.ts local fixes + E# wired up (7 scopes, connected) info open

Company E# — DONE

Authorized with all 7 scopes (customer, invoice, supplier, supplierinvoice, bookkeeping, companyinformation, order). MCP server fortnox-e added at user scope (~/.claude.json), status ✓ Connected, with its own token cache C:\Users\DanielRhodin\.fortnox-mcp\tokens-e.json (via FORTNOX_TOKEN_FILE [[local-patch-fortnox-token-file]]).

  • NOTE: "Connected" only means the server booted with a token present; the token is validated lazily on first API call. Still needs a read-only verification after a Claude Code restart.

Local fixes to scripts/get-token.ts (not upstreamed — re-apply after git pull; candidates to PR back)

  1. OAuth param order — Fortnox's auth endpoint is order-sensitive; reordered to client_id, redirect_uri, scope, state, access_type, response_type. Fixed the initial invalid_request / "Invalid or missing response type" error. (The stock repo order is broken.)
  2. Default scopes expanded from 5 to 7 (added supplierinvoice, order) so all server tools are covered.
  3. FORTNOX_SCOPES env override — space-separated, lets per-company scope trimming for companies lacking modules. Defaults to the 7.

Process notes

  • get-token must be run as a true background process (Bash/PowerShell run_in_background); Start-Process from a tool shell gets killed when the shell exits → "localhost refused to connect".
  • Redirect URI registered in the Fortnox app: http://localhost:8888/callback.
  • Fortnox refresh tokens rotate on use; first MCP run consumes the env token and persists the rotated one to the token file.
E-010 Deployment LIVE on factory.local:9000 — Docker stack verified end-to-end info open

The shared Fortnox MCP server is deployed and operational on factory.local, implementing the architecture decided in [[E-009]].

What's running

  • Two containers via docker compose (project fortnox-mcp, separate from the workflowengine stack) at ~/fortnox-mcp on factory.local:
    • fortnox-mcp — Node MCP server, local/HTTP mode, internal port 3000 (not published), healthy.
    • caddy — reverse proxy publishing host port 9000, enforcing the shared bearer token.
  • Endpoint for colleagues: http://factory.local:9000/mcp (LAN/mDNS).

Verified

  • /health OK; POST /mcp without token → 401; with token → MCP handshake OK.
  • Real Fortnox call fortnox_get_company_info returned E-Sharp AB data through the full chain (token activated).
  • Image built from the branch carrying the reliability fixes ([[E-008]] / PR #6).

Port selection

Picked 9000 after scanning factory.local — 8080 was already taken by workflowengine-orchestra. Configurable via MCP_HOST_PORT in .env.

Secrets / operations (NOT in git)

  • Server .env (perms 600) holds client id/secret, the dedicated server refresh token, and the bearer token. The rotating refresh token persists in the fortnox-tokens Docker volume (/data/tokens.json).
  • The shared bearer token is held in the server .env; distribute to colleagues out-of-band (it's their Authorization: Bearer value).
  • Server authenticated via a dedicated passphraseless deploy key (~/.ssh/factory_deploy) — independent of the user's id_ed25519.

Remaining / optional

  • Commit + push the deploy files (Dockerfile, docker-compose.yml, Caddyfile, .env.example, .dockerignore, DEPLOYMENT.md) to the fork — pending decision on same vs separate PR from the reliability fixes.
  • Plain HTTP on LAN (bearer sniffable on-wire); TLS is the upgrade path if needed.
E-011 How to get access: connecting to the shared Fortnox MCP server info open

Step-by-step for a colleague to connect their AI client to the shared Fortnox MCP server. See [[E-010]] for the deployment and [[E-009]] for the security model.

What you need

  1. On the LAN (office network or VPN) — the server is factory.local, not exposed to the internet.
  2. The shared bearer token — ask Daniel (it lives in the server's .env, MCP_BEARER_TOKEN). Treat it like a password.
  3. Endpoint: http://factory.local:9000/mcp (if factory.local doesn't resolve on your machine, use the server's LAN IP instead).

Claude Code

claude mcp add --transport http fortnox http://factory.local:9000/mcp \
  --header "Authorization: Bearer <SHARED_TOKEN>"

Claude Desktop

Edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Windows: %APPDATA%\Claude\):

{
  "mcpServers": {
    "fortnox": {
      "type": "http",
      "url": "http://factory.local:9000/mcp",
      "headers": { "Authorization": "Bearer <SHARED_TOKEN>" }
    }
  }
}

Restart Claude Desktop.

Verify it works

Ask the assistant: "Use Fortnox to show the company info." You should get E-Sharp AB back. Or from a terminal on the LAN:

curl -s http://factory.local:9000/health        # -> {"status":"ok",...}

Notes / troubleshooting

  • 401 Unauthorized → missing/wrong bearer token in the Authorization header.
  • Can't connect / DNS → not on the LAN/VPN, or factory.local mDNS not resolving (use the IP).
  • Everyone shares one Fortnox account (E-Sharp AB) and the full toolset (read + write/bookkeep) — there is no per-user identity, so all actions appear in Fortnox as the one service login. Be careful with write operations.
  • Plain HTTP on the LAN: keep the token on a trusted network.

Design Rule Status

No violations   Warning   Error   Waived