MCP making it so we can have Claude e.g. automatically find receipts for us
Find a file
Claude a73af353d4 Harden server; add authenticated Streamable HTTP transport and Nix module
- Streamable HTTP transport (MCP_TRANSPORT=http) with mandatory auth:
  static bearer tokens or OIDC resource-server mode (JWT via JWKS, or
  RFC 7662 introspection), RFC 9728 protected-resource metadata, and
  DNS-rebinding protection
- secrets loadable from files via *_FILE variants
- timeouts on all outbound requests; JORTT_READ_ONLY tool filtering
- OpenAPI spec committed and bundled; no implicit network fetches
- Nix flake: sandboxed package build + hardened NixOS module taking an
  EnvironmentFile (agenix-friendly)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:10:55 +02:00
nix Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
scripts Add Jortt API MCP server 2026-07-11 14:14:20 +00:00
src Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
.env.example Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
.gitignore Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
flake.lock Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
flake.nix Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
LICENSE Add Jortt API MCP server 2026-07-11 14:14:20 +00:00
package-lock.json Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
package.json Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
README.md Harden server; add authenticated Streamable HTTP transport and Nix module 2026-07-11 19:10:55 +02:00
tsconfig.json Add Jortt API MCP server 2026-07-11 14:14:20 +00:00

jortt-mcp

A Model Context Protocol (MCP) server for the Jortt accounting & invoicing API.

It exposes the full Jortt REST API — customers, invoices, estimates, expenses, projects, bank accounts, reports, and more — as MCP tools, so an MCP-capable assistant (Claude Desktop, Claude Code, etc.) can read and write your Jortt administration on your behalf.

The tools are generated directly from Jortt's published OpenAPI specification, so coverage stays faithful to the API: 82 tools across every documented resource.

How it works

  • The Jortt OpenAPI spec is committed at src/openapi.json and bundled into dist/ at build time. On startup the server reads that spec, resolves all schema references, and turns every operation into an MCP tool with a proper JSON Schema for its arguments. Refresh the spec explicitly with npm run fetch-spec; the server never loads it from the network unless you set JORTT_OPENAPI_URL deliberately.
  • Authentication towards Jortt uses the OAuth 2.0 client credentials grant (the flow Jortt recommends for applications bound to your own administration). The server fetches a bearer token, caches it until shortly before it expires, and transparently refreshes on expiry or a 401.
  • Path parameters, query parameters, and JSON request bodies are all handled automatically. Request bodies are passed under a single body argument.
  • Two transports: stdio (default, for local clients like Claude Desktop) and Streamable HTTP (MCP_TRANSPORT=http) for remote access. The HTTP transport refuses to start without authentication configured.

Prerequisites

  1. A Jortt account.
  2. A registered API application. Create one under Profile → APIOAuth applications, selecting the scopes you want to grant. Registration gives you a Client ID and Client secret.

Installation

git clone https://github.com/deprekated/jortt-mcp.git
cd jortt-mcp
npm install
npm run build

On Nix, nix build github:deprekated/jortt-mcp — or see NixOS below for the module.

Configuration

The server is configured entirely through environment variables. Every secret-bearing variable can instead be read from a file by appending _FILE (e.g. JORTT_CLIENT_SECRET_FILE=/run/agenix/jortt-client-secret), which keeps secrets out of process environments and works directly with agenix / systemd credentials.

Core

Variable Required Description
JORTT_CLIENT_ID yes OAuth client ID of your registered application.
JORTT_CLIENT_SECRET yes OAuth client secret.
JORTT_SCOPES no Space-separated scopes to request. Defaults to all scopes; the token server only grants scopes your application was registered with.
JORTT_READ_ONLY no 1/true: expose only GET (list/get) tools — a hard stop on writes regardless of scopes.
JORTT_OPENAPI_URL no Load the OpenAPI spec from this URL instead of the bundled copy. Off by default.

HTTP transport & authentication

Variable Description
MCP_TRANSPORT stdio (default) or http (Streamable HTTP).
MCP_HOST / MCP_PORT Bind address and port. Defaults: 127.0.0.1:3910. The server speaks plain HTTP — terminate TLS in a reverse proxy.
MCP_AUTH_MODE token or oidc. Required in http mode; the server refuses to run unauthenticated.
MCP_AUTH_TOKEN Static bearer token(s), comma/newline separated (token mode). Generate with openssl rand -hex 32. Compared in constant time; minimum 16 chars.
MCP_OIDC_ISSUER Issuer URL of your OAuth authorization server / proxy (oidc mode).
MCP_RESOURCE_URL Public URL of the MCP endpoint (e.g. https://jortt-mcp.example.com/mcp). Used as token audience and published in RFC 9728 protected-resource metadata for MCP client discovery.
MCP_OIDC_AUDIENCE Override the audience to require in tokens (default: MCP_RESOURCE_URL).
MCP_OIDC_ALLOWED_SUBJECTS Comma-separated allowlist matched against sub, preferred_username, and email. Strongly recommended — without it, any account on your IdP is accepted.
MCP_OIDC_INTROSPECTION_URL, MCP_OIDC_CLIENT_ID, MCP_OIDC_CLIENT_SECRET Use RFC 7662 token introspection instead of JWT validation, for issuers with opaque access tokens (e.g. Authelia). The endpoint is discovered from the issuer when the URL is omitted.
MCP_ALLOWED_HOSTS / MCP_ALLOWED_ORIGINS Exact-match allowlists for the Host / Origin headers (DNS-rebinding protection). Include the public hostname (with :port if non-standard).

In oidc mode the server is a standard OAuth 2.0 resource server per the MCP authorization spec: it validates bearer tokens issued by your IdP/OAuth proxy (Keycloak, Authelia, Pomerium, Kanidm, ...) and serves /.well-known/oauth-protected-resource so clients like the claude.ai remote-connector UI can discover the authorization server and run the authorization-code + PKCE flow against it. For claude.ai, your IdP needs either dynamic client registration or a manually registered client whose ID/secret you paste into the connector settings; the redirect URL to register is https://claude.ai/api/mcp/auth_callback.

Endpoints in http mode: POST/GET/DELETE /mcp (MCP, authenticated), GET /healthz (unauthenticated liveness probe), GET /.well-known/oauth-protected-resource (only when OIDC is configured).

Claude Desktop / Claude Code

Add the server to your MCP client configuration (for Claude Desktop, this is claude_desktop_config.json):

{
  "mcpServers": {
    "jortt": {
      "command": "node",
      "args": ["/absolute/path/to/jortt-mcp/dist/index.js"],
      "env": {
        "JORTT_CLIENT_ID": "your-client-id",
        "JORTT_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}

Restrict what the assistant can do by narrowing the scopes:

"env": {
  "JORTT_CLIENT_ID": "your-client-id",
  "JORTT_CLIENT_SECRET": "your-client-secret",
  "JORTT_SCOPES": "invoices:read customers:read reports:read"
}

Remote (Streamable HTTP)

With a static token, from Claude Code:

claude mcp add --transport http jortt https://jortt-mcp.example.com/mcp \
  --header "Authorization: Bearer <token>"

With OIDC, add https://jortt-mcp.example.com/mcp as a custom connector on claude.ai — the OAuth flow against your IdP is discovered automatically.

NixOS

The repository is a flake providing a package and a NixOS module. The module runs the server as a hardened systemd service (DynamicUser, syscall filtering, read-only filesystem view) listening on loopback; put nginx/caddy in front for TLS. Secrets are supplied via a systemd EnvironmentFile, which is where agenix comes in:

{
  inputs.jortt-mcp.url = "github:deprekated/jortt-mcp";

  # in your host configuration:
  imports = [ inputs.jortt-mcp.nixosModules.default ];

  age.secrets.jortt-mcp-env.file = ./secrets/jortt-mcp.env.age;
  # The encrypted file contains:
  #   JORTT_CLIENT_ID=...
  #   JORTT_CLIENT_SECRET=...
  #   MCP_AUTH_TOKEN=...            # token mode only
  #   MCP_OIDC_CLIENT_SECRET=...    # only for introspection mode

  services.jortt-mcp = {
    enable = true;
    environmentFile = config.age.secrets.jortt-mcp-env.path;
    port = 3910;

    # Simplest: static bearer token (set MCP_AUTH_TOKEN in the env file)
    auth.mode = "token";

    # Or: OAuth via your IdP / OAuth proxy, as needed for claude.ai
    # auth.mode = "oidc";
    # auth.oidc.issuer = "https://auth.example.com";
    # auth.oidc.allowedSubjects = [ "kate" ];
    # resourceUrl = "https://jortt-mcp.example.com/mcp";

    allowedHosts = [ "jortt-mcp.example.com" ];
    # readOnly = true;
    # scopes = [ "invoices:read" "customers:read" "reports:read" ];
  };

  services.nginx.virtualHosts."jortt-mcp.example.com" = {
    enableACME = true;
    forceSSL = true;
    locations."/".proxyPass = "http://127.0.0.1:3910";
    # SSE responses must not be buffered:
    locations."/".extraConfig = ''
      proxy_buffering off;
      proxy_read_timeout 1h;
    '';
  };
}

If your IdP issues opaque access tokens (Authelia does), switch the OIDC block to introspection:

auth.oidc = {
  issuer = "https://auth.example.com";
  introspectionClientId = "jortt-mcp";
  allowedSubjects = [ "kate" ];
};
# and put MCP_OIDC_CLIENT_SECRET=... in the agenix env file

Tool overview

Tool names follow a predictable verb_resource convention derived from the API path and version. A selection:

Tool Method & path What it does
list_customers GET /customers List / search customers
create_customers POST /customers Create a customer
get_customers GET /customers/{customer_id} Fetch a single customer
list_invoices GET /invoices List / search invoices
create_v3_invoices POST /v3/invoices Create an invoice (invoice-level VAT)
send_invoices POST /invoices/{id}/send Send an invoice
get_invoices_download GET /invoices/{id}/download Get a download URL for the invoice PDF
list_v2_estimates GET /v2/estimates List estimates
create_v2_estimates POST /v2/estimates Create an estimate
list_v3_expenses GET /v3/expenses List expenses
list_v3_bank_accounts GET /v3/bank-accounts List bank accounts
get_reports_summaries_profit_and_loss GET /reports/summaries/profit_and_loss Dashboard profit & loss summary
get_organizations_me GET /organizations/me The organization tied to your credentials

Newer resources are versioned (v2, v3) and the version is part of the tool name. Where Jortt offers multiple versions of the same operation (for example invoice creation), prefer the highest version unless you have a reason not to — create_v3_invoices supports invoice-level VAT.

Run the server and call tools/list to see the full, always-current set with per-tool argument schemas.

Development

npm run build      # compile TypeScript to dist/ (and copy the spec)
npm run dev        # tsc --watch
npm run typecheck  # type-check without emitting

Updating to the latest API

The tool set is generated from the committed OpenAPI spec. To pick up new Jortt endpoints, refresh it and rebuild:

npm run fetch-spec   # downloads https://api.jortt.nl/swagger_doc to src/openapi.json
npm run build

No code changes are needed — new operations become new tools automatically. Review and commit the updated src/openapi.json.

Security model

  • The MCP endpoint fronts your entire administration. Whoever can call it can do whatever the Jortt application's scopes allow. Layer the controls: narrow the scopes at registration, set JORTT_READ_ONLY if writes aren't needed, and in OIDC mode always set MCP_OIDC_ALLOWED_SUBJECTS.
  • The HTTP transport refuses to start without MCP_AUTH_MODE; there is no unauthenticated mode.
  • Static tokens are compared in constant time (over SHA-256 digests); JWTs are validated for signature, issuer, expiry, and audience; introspection results are cached for at most 60 s so revocation takes effect quickly.
  • The server binds to loopback by default and expects a TLS-terminating reverse proxy in front. MCP_ALLOWED_HOSTS/MCP_ALLOWED_ORIGINS add DNS-rebinding protection.
  • Secrets can be read from files (*_FILE) instead of the environment; the NixOS module keeps them in a root-owned EnvironmentFile (e.g. agenix) and runs the service fully sandboxed.
  • All outbound HTTP calls (Jortt API, token endpoint, JWKS/introspection) carry timeouts. The OpenAPI spec that defines the tool surface is bundled at build time, never fetched implicitly at runtime.

Notes & limits

  • Jortt rate-limits the API to ~10 requests/second. List endpoints paginate at a maximum of 100 items per page (page argument).
  • This is an unofficial, community project and is not affiliated with or endorsed by Jortt.

License

MIT — see LICENSE.