npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codemill-solutions/twinfield-mcp

v0.4.0

Published

MCP server for the Twinfield accounting SOAP API

Readme

twinfield-mcp

A Model Context Protocol (MCP) server that connects AI agents to Twinfield accounting via Twinfield's SOAP API.

Built with Node.js, TypeScript, and @modelcontextprotocol/sdk.

Status: v0.4.0 — full read + write surface. Authentication, office details, dimension reads (now covering both BAS balance-sheet and PNL profit-and-loss accounts), browse-based transaction reads, dimension upserts + deactivation, and the three core financial writes: process_journal, process_sales_invoice, process_purchase_invoice. All writes default to destiny="temporary" (draft) for safety. Document upload is planned for v0.5+.


Installation

npm install @codemill-solutions/twinfield-mcp

Then add it to your MCP host configuration (e.g. claude_desktop_config.json):

{
  "mcpServers": {
    "twinfield": {
      "command": "node",
      "args": ["node_modules/@codemill-solutions/twinfield-mcp/dist/index.js"],
      "env": {
        "TWINFIELD_OFFICE_CODE": "your-office-code"
      }
    }
  }
}

The actual OAuth2 credentials (client id, client secret, 25-year refresh token) live in ~/.twinfield/credentials.json rather than environment variables — see Setup below.


Prerequisites

  • Node.js 20+
  • A Twinfield account with API access enabled
  • An OpenID Connect client registered via the Twinfield Developer Portal
    • Authorization flow: authorization code
    • Access token type: JWT
    • Redirect URL: http://localhost:8765/callback
    • Scopes that will be requested: openid twf.user twf.organisation twf.organisationUser offline_access

Setup

1. Install dependencies

npm install

2. Run the one-time authorization

npm run authorize

The interactive script:

  1. Asks for client_id, client_secret, and the office code (CompanyCode) you want to associate.
  2. Opens your browser to the Twinfield login page on https://login.twinfield.com.
  3. Receives the authorization code on http://localhost:8765/callback.
  4. Exchanges the code for an access + refresh token.
  5. Calls Twinfield's access-token-validation endpoint to discover the per-account cluster URL.
  6. Writes ~/.twinfield/credentials.json (mode 0600) with the office entry.

Twinfield refresh tokens have a 25-year TTL, so this is a one-time setup. After this step the MCP server can authenticate non-interactively forever (or until you reset the client secret in the developer portal).

3. Build

npm run build

4. Connect to an MCP host

{
  "mcpServers": {
    "twinfield": {
      "command": "node",
      "args": ["/absolute/path/to/twinfield-mcp/dist/index.js"],
      "env": {
        "TWINFIELD_OFFICE_CODE": "your-office-code"
      }
    }
  }
}

Multi-office support

A single OAuth client typically grants access to all offices (CompanyCodes) within one organisation. Call list_offices to discover which office codes you can use. Every read tool accepts an office parameter that overrides the default for that one call.

If you manage multiple organisations (each with its own client_id/client_secret pair), supply a JSON file that maps every office code to its OAuth2 credentials. The server then authenticates per office automatically — no single shared refresh token required.

Credentials file format

{
  "OFFICE_CODE_A": {
    "clientId": "...",
    "clientSecret": "...",
    "refreshToken": "..."
  },
  "OFFICE_CODE_B": {
    "clientId": "...",
    "clientSecret": "...",
    "refreshToken": "..."
  }
}

The file should be chmod 600 — it contains long-lived refresh tokens. npm run authorize sets this automatically when it writes the file.

Path resolution (first match wins)

| Priority | Path | |----------|------| | 1 | TWINFIELD_CREDENTIALS_FILE environment variable (explicit path) | | 2 | ~/.twinfield/credentials.json (default user-level location) | | 3 | ./credentials.json (local fallback for development) |

Reloading credentials at runtime

When a new office entry is added externally — e.g. by running npm run authorize from a sibling tool — the file change is not yet visible to a running MCP server. The reload_credentials tool re-reads the JSON file from disk and replaces the in-memory map in place. Tokens for offices that changed or were removed are evicted from the token cache automatically; tokens for unchanged offices stay warm so subsequent calls do not pay the refresh cost.


Available tools (18)

Authentication & setup

| Tool | Description | |------|-------------| | whoami | Validate Twinfield authentication for an office. Calls the OpenID Connect userinfo endpoint and returns the organisation claims. Run this first to confirm credentials, cluster discovery, and the refresh-token flow all work end-to-end. | | reload_credentials | Re-read the office → credentials JSON file from disk without restarting the server. Returns a diff of added/updated/removed office codes and invalidates affected tokens. |

Offices

| Tool | Description | |------|-------------| | list_offices | List all Twinfield offices (CompanyCodes) accessible with the current OAuth credentials. Run this after whoami to discover which office codes can be passed as the office parameter to other tools. | | get_office | Read full details for a single office: base currency, VAT/CoC numbers, default bank, region, address, fiscal config. Returns a curated summary plus the full raw response under details. |

Dimensions (master data)

Twinfield models customers, suppliers, GL accounts, cost centres, and projects as "dimensions" with a 3-letter type code. Each tool below is a thin wrapper over <list><type>dimensions</type><dimtype>…</dimtype></list> with a fixed dimtype.

| Tool | Dimtype | Description | |------|---------|-------------| | get_customers | DEB | List all customers (debtors) for an office. | | get_suppliers | CRD | List all suppliers / vendors (creditors) for an office. | | get_gl_accounts | BAS + PNL | List all GL accounts. Combines balance-sheet (BAS) and profit-and-loss (PNL) into one response; each entry includes glType so revenue/cost lines (PNL) are distinguishable from balance positions (BAS). Optional glType parameter narrows to one side. | | get_cost_centers | KPL | List all cost centres for an office. | | get_projects | PRJ | List all projects for an office. |

All dimension tools return an array of { code, name?, shortname?, glType? } entries.

Transactions (browse queries)

Built on Twinfield's <columns code="100"> browse query. Each row in the response is one transaction line with daybook, number, date, year-period, counterparty (fin.trs.line.dim2), match status, signed amount, and signed open amount.

| Tool | Default daybook | Description | |------|-----------------|-------------| | get_transactions | — | List transactions filtered by daybook code, year-period range, and/or counterparty. Run without filters to discover the daybook codes used on this office. | | get_sales_invoices | VRK | Sales invoice lines. Pass openOnly=true to keep only unpaid lines. | | get_purchase_invoices | INK | Purchase invoice lines. Pass openOnly=true to keep only unpaid lines. |

Common parameters for all three:

  • office?: string — override the default office.
  • daybook?: string — Twinfield daybook code (VRK, INK, BNK, KAS, MEMO, …). Overrides the per-tool default.
  • yearperiodFrom?: string, yearperiodTo?: string — inclusive range in YYYY/PP format (e.g. 2024/01 to 2024/12). Must be supplied together.
  • counterparty?: string — filter to a single customer/supplier code.
  • openOnly?: boolean — client-side post-filter that keeps only rows whose match status is available (only on get_sales_invoices / get_purchase_invoices).

Note on daybook codes. VRK and INK are the Dutch defaults (Verkoop / Inkoop). Offices on a non-Dutch Twinfield template may use different codes — run get_transactions once without filters and inspect the daybook field on the result to see what your office uses.

Write tools — master data

| Tool | Description | |------|-------------| | upsert_customer | Create or update a customer (Twinfield dimension type DEB). Idempotent on code. The allowed code format depends on the office configuration — Twinfield surfaces the exact pattern in the error message when the format is wrong. | | upsert_supplier | Create or update a supplier (Twinfield dimension type CRD). Same shape as upsert_customer. | | deactivate_dimension | Soft-delete a customer / supplier / cost-centre / project by marking it inactive (Twinfield does not allow true deletes for dimensions with transaction history). The current name is preserved automatically — Twinfield requires it on every dimension upsert. |

Write tools — transactions

| Tool | Description | |------|-------------| | process_journal | Post a general journal entry (memoriaal) via <transaction destiny="…">. Validates client-side that lines balance to zero. Dimension codes are auto-padded to 4 digits where needed. | | process_sales_invoice | Book a sales invoice via the VRK daybook. Composes the <transaction> with <invoicenumber> + optional <duedate>, a type="total" debtor line (default GL 1300), and one or more revenue lines with optional <vatcode> (sales codes start with V: VH = 21%, VL = 9%, VN = 0% / vrijgesteld). Twinfield auto-generates the VAT booking from the code. | | process_purchase_invoice | Symmetric sibling: posts to the INK daybook with a creditor total line (default GL 1600) and cost lines using purchase-side VAT codes (IH, IL, IN). |

All transaction writes default to destiny="temporary" (draft) — the entry lands in Twinfield's UI as an editable proposal you can review and finalise. Pass destiny="final" to commit immediately.

Why destiny="temporary" is the default. Twinfield bookings are hard to unwind once final. The temporary status lets an agent propose an entry that you (the human) review and accept in the Twinfield UI before it touches the books. Override only when you have a deterministic write you trust.

Sales vs. purchase VAT codes are NOT interchangeable. Twinfield uses two distinct prefixes — V* (Verkoop / sales) and I* (Inkoop / purchase). Using VH on a purchase invoice errors with "BTW Hoog (VH) is van het type Verkoop terwijl het dagboek van het btw-type Inkoop is." The tools default to sensible per-daybook codes but pass whatever your account uses.


Testing

MCP Inspector (tool-level, no LLM)

npm run inspect

Opens a browser UI where you can call individual tools and inspect raw responses.

Standalone probes

For quick command-line validation without the MCP layer:

npx tsx scripts/whoami.ts            # exercises refresh + cluster + userinfo
npx tsx scripts/list-offices.ts      # exercises the ProcessXml SOAP path

Architecture

src/
├── index.ts                  # Entry point — loads env + credentials, registers tools, starts stdio transport
├── twinfield-client.ts       # OAuth2 token cache, cluster discovery, SOAP envelope, ProcessXml call, fair-use handling
└── tools/
    ├── auth.ts               # whoami, reload_credentials
    ├── offices.ts            # list_offices
    ├── dimensions.ts         # get_customers, get_suppliers, get_gl_accounts,
    │                         # get_cost_centers, get_projects
    └── transactions.ts       # get_transactions, get_sales_invoices,
                              # get_purchase_invoices

scripts/
├── authorize.ts              # One-time interactive OAuth2 authorization-code flow
├── whoami.ts                 # Standalone auth-chain probe
└── list-offices.ts           # Standalone ProcessXml probe

Auth flow

Twinfield uses OpenID Connect (authorization code + refresh token). The server-side flow:

  1. npm run authorize runs the authorization code grant once per office, captures the refresh token, and writes it to ~/.twinfield/credentials.json.
  2. At runtime, TwinfieldClient.getAccessToken(office) exchanges the refresh token for a fresh access token (1-hour TTL) and caches it. The cache is refreshed ~30 seconds before expiry to absorb clock skew.
  3. The cluster URL (https://api.<cluster>.twinfield.com) is discovered by calling Twinfield's accesstokenvalidation endpoint, which returns the twf.clusterUrl claim. It's cached alongside the access token.
  4. Every business call goes to {cluster}/webservices/processxml.asmx with a SOAP header containing AccessToken + CompanyCode + CompanyId xsi:nil="true".

ProcessXml envelope

Twinfield's ProcessXmlString method takes a single xs:string parameter. The Twinfield XML payload (<list>, <read>, <columns>, etc.) must therefore be escaped as character data inside <xmlRequest>. The response is similarly a string containing escaped XML — the client re-parses it so tools see a structured object.

The SOAP header is the OAuth2 variant of Twinfield's legacy session-based header:

<soap:Header>
  <Header xmlns="http://www.twinfield.com/">
    <AccessToken>...</AccessToken>
    <CompanyCode>YOUR-OFFICE-CODE</CompanyCode>
    <CompanyId xsi:nil="true" />
  </Header>
</soap:Header>

CompanyId is minOccurs="1" in the WSDL but nillable="true" — leaving it out causes a generic HTTP 400 with no SOAP fault body.


Rate limits

Twinfield enforces a credit-based fair-use policy (HTTP 429 with Retry-After when exceeded):

| Bucket | Certified clients | Uncertified clients | |---|---|---| | Per ClientId | 1000 credits/min | 50 credits/min | | Per ClientId + Organisation | 500 credits/min | 25 credits/min | | Per IP | 1000 credits/min | 1000 credits/min |

Query requests (read tools) cost 1 credit; mutations cost 3. Concurrency is capped at 20 in-flight requests per ClientId / 10 per Organisation. Transactions are hard-capped at 1000 lines (HTTP 400 if exceeded).

A fresh OAuth client is uncertified by default. The 50/min budget is enough for interactive agent usage but you'll want to design batch workflows to fetch broad lists once rather than re-fetching on every step. TwinfieldClient honours Retry-After with one bounded retry on 429.


Troubleshooting

| Error | Likely cause | |-------|-------------| | Twinfield OAuth error during refresh token exchange — invalid_grant | Refresh token was invalidated — re-run npm run authorize for the affected office. | | Twinfield token-validation response did not include a usable twf.clusterUrl claim | Access token is missing the twf.organisation scope — re-authorize. | | No Twinfield credentials configured for office "..." | The office code isn't in ~/.twinfield/credentials.json — run npm run authorize for it, then call reload_credentials. | | HTTP 400 Bad Request from .../processxml.asmx (no body) | The SOAP envelope is malformed in a way that fails Twinfield's WCF deserializer before any handler runs. Usually a header field missing or an unescaped <xmlRequest>. | | SOAP Fault: An error occurred on the server. (with reference code) | Twinfield server-side error — note the reference code (YYYY-MM-DD CXXXXXX) and contact Twinfield support. Often caused by a malformed <columns> browse payload. | | Type niet geïmplementeerd. | The <list> or <read> type you requested isn't supported on the ProcessXml endpoint. Many entities are only exposed via other SOAP services (Finder, BankBook, Documents) — not yet wrapped by this MCP. | | HTTP 429 with Retry-After | Fair-use credit budget exceeded — the client retries once automatically, then surfaces the error. Reduce request rate or apply for client certification. |


About CodeMill Solutions

CodeMill Solutions is a Dutch software company based in the Netherlands. We build smart, scalable, and customized solutions that help organizations grow, optimize processes, and realize their digital ambitions.

Our services include:

  • Custom applications — portals, dashboards, business software, and fully tailored platforms that truly add value.
  • API integrations — connecting your application with other systems and external platforms via smart API connections.
  • Mobile apps — iOS and Android apps as a logical extension of your web application(s).

twinfield-mcp is one of our open-source integrations, making Twinfield's accounting platform accessible to AI agents through the Model Context Protocol.

📧 [email protected] 🌐 codemill.dev 💼 LinkedIn 🐙 GitHub


License

MIT — see LICENSE.