{"_id":"@codemill-solutions/twinfield-mcp","_rev":"6-6a4b57fdd491a1c200a56099bd7a264c","name":"@codemill-solutions/twinfield-mcp","dist-tags":{"latest":"0.4.0"},"versions":{"0.1.0":{"name":"@codemill-solutions/twinfield-mcp","version":"0.1.0","keywords":["mcp","model-context-protocol","twinfield","accounting","boekhouden","soap","ai","agent","anthropic","claude"],"author":{"name":"CodeMill Solutions B.V."},"license":"MIT","_id":"@codemill-solutions/twinfield-mcp@0.1.0","maintainers":[{"name":"codemillsolutions","email":"dev@codemill.dev"}],"homepage":"https://github.com/CodeMill-Solutions/twinfield-mcp#readme","bugs":{"url":"https://github.com/CodeMill-Solutions/twinfield-mcp/issues"},"dist":{"shasum":"832702d212e2b5ae9331aa42133f205cbcf62083","tarball":"https://registry.npmjs.org/@codemill-solutions/twinfield-mcp/-/twinfield-mcp-0.1.0.tgz","fileCount":23,"integrity":"sha512-YyrwyNE+RqYoARWzhil80pOeSngl12CC3aIbTUX2xGK9drgLjG0HNKr1hYj7eeS1PqInFa6uuXFT7KmwJKkWXw==","signatures":[{"sig":"MEUCIGg2BFxtd5qC6K7beQ/ICdvgXsdSiLgtkD/pZN0PKof3AiEAvi0pwTH6W5HDiGH8xXFPffu0B4YNYo5rbXxWc3q1his=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":105900},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"e2eca74efaa4e0119f749c733011a1140e20f6c3","scripts":{"dev":"tsx src/index.ts","agent":"npx tsx scripts/test-agent.ts","build":"tsc","start":"node dist/index.js","inspect":"npx @modelcontextprotocol/inspector node dist/index.js","authorize":"npx tsx scripts/authorize.ts","prepublishOnly":"npm run build"},"_npmUser":{"name":"codemillsolutions","email":"dev@codemill.dev"},"deprecated":"Use 0.1.1 or later — 0.1.0 contained a real office code in JSDoc comments.","repository":{"url":"git+https://github.com/CodeMill-Solutions/twinfield-mcp.git","type":"git"},"_npmVersion":"10.8.2","description":"MCP server for the Twinfield accounting SOAP API","directories":{},"_nodeVersion":"20.20.2","dependencies":{"zod":"^3.25.76","axios":"^1.16.1","dotenv":"^16.6.1","fast-xml-parser":"^5.8.0","@anthropic-ai/sdk":"^0.78.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.3","prettier":"^3.8.3","typescript":"^5.9.3","@types/node":"^22.19.19"},"_npmOperationalInternal":{"tmp":"tmp/twinfield-mcp_0.1.0_1779884172929_0.36781004984149157","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@codemill-solutions/twinfield-mcp","version":"0.1.1","keywords":["mcp","model-context-protocol","twinfield","accounting","boekhouden","soap","ai","agent","anthropic","claude"],"author":{"name":"CodeMill Solutions B.V."},"license":"MIT","_id":"@codemill-solutions/twinfield-mcp@0.1.1","maintainers":[{"name":"codemillsolutions","email":"dev@codemill.dev"}],"homepage":"https://github.com/CodeMill-Solutions/twinfield-mcp#readme","bugs":{"url":"https://github.com/CodeMill-Solutions/twinfield-mcp/issues"},"dist":{"shasum":"2174dd4f56084eac003a845718948ba1039594a2","tarball":"https://registry.npmjs.org/@codemill-solutions/twinfield-mcp/-/twinfield-mcp-0.1.1.tgz","fileCount":23,"integrity":"sha512-ElWuirYUPthqkuHYuDSGFN3M9NSvkoSNHPQZEu/A1LDkC4/p0EK6CW1ZHNTYcvxs5MWmPyFkKKz52XL5t/m5Bw==","signatures":[{"sig":"MEQCIHJqJRs/W8QazwIfapfaM/gxUfQSILAebx+vxWz/R32XAiB6i4sIwWwf5ZnTih+43LjXOijPXZxv7uWH+0HuMYn3Jw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":105919},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"628c9245ff700203cd6855c6c94c7d890d20c921","scripts":{"dev":"tsx src/index.ts","agent":"npx tsx scripts/test-agent.ts","build":"tsc","start":"node dist/index.js","inspect":"npx @modelcontextprotocol/inspector node dist/index.js","authorize":"npx tsx scripts/authorize.ts","prepublishOnly":"npm run build"},"_npmUser":{"name":"codemillsolutions","email":"dev@codemill.dev"},"repository":{"url":"git+https://github.com/CodeMill-Solutions/twinfield-mcp.git","type":"git"},"_npmVersion":"10.8.2","description":"MCP server for the Twinfield accounting SOAP API","directories":{},"_nodeVersion":"20.20.2","dependencies":{"zod":"^3.25.76","axios":"^1.16.1","dotenv":"^16.6.1","fast-xml-parser":"^5.8.0","@anthropic-ai/sdk":"^0.78.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.3","prettier":"^3.8.3","typescript":"^5.9.3","@types/node":"^22.19.19"},"_npmOperationalInternal":{"tmp":"tmp/twinfield-mcp_0.1.1_1779884519666_0.06761344663329538","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@codemill-solutions/twinfield-mcp","version":"0.2.0","keywords":["mcp","model-context-protocol","twinfield","accounting","boekhouden","soap","ai","agent","anthropic","claude"],"author":{"name":"CodeMill Solutions B.V."},"license":"MIT","_id":"@codemill-solutions/twinfield-mcp@0.2.0","maintainers":[{"name":"codemillsolutions","email":"dev@codemill.dev"}],"homepage":"https://github.com/CodeMill-Solutions/twinfield-mcp#readme","bugs":{"url":"https://github.com/CodeMill-Solutions/twinfield-mcp/issues"},"dist":{"shasum":"321e28d49ccd0852dd703b82b318b0233b887b35","tarball":"https://registry.npmjs.org/@codemill-solutions/twinfield-mcp/-/twinfield-mcp-0.2.0.tgz","fileCount":27,"integrity":"sha512-3RSGeY1Cbi+E8ghef5BZLzO84wE/Dr99iQd6wruTNM/YTBWsP+7ZM2JpR1d2UmU79pKYhFmtD35qayKLIfCGCA==","signatures":[{"sig":"MEYCIQDETs2s4jfDuHhtca5ziuo/vHviAP5LF4j3mSUQX9SZFQIhAM0mPLj5+SBTt3cSRBasRr1NQMUzrKOjDCmYvUlcdP/u","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137090},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"574442990a24620869a368eed6ec4dfb7e091c4a","scripts":{"dev":"tsx src/index.ts","agent":"npx tsx scripts/test-agent.ts","build":"tsc","start":"node dist/index.js","inspect":"npx @modelcontextprotocol/inspector node dist/index.js","authorize":"npx tsx scripts/authorize.ts","prepublishOnly":"npm run build"},"_npmUser":{"name":"codemillsolutions","email":"dev@codemill.dev"},"repository":{"url":"git+https://github.com/CodeMill-Solutions/twinfield-mcp.git","type":"git"},"_npmVersion":"10.8.2","description":"MCP server for the Twinfield accounting SOAP API","directories":{},"_nodeVersion":"20.20.2","dependencies":{"zod":"^3.25.76","axios":"^1.16.1","dotenv":"^16.6.1","fast-xml-parser":"^5.8.0","@anthropic-ai/sdk":"^0.78.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.3","prettier":"^3.8.3","typescript":"^5.9.3","@types/node":"^22.19.19"},"_npmOperationalInternal":{"tmp":"tmp/twinfield-mcp_0.2.0_1779887897191_0.5551539334551958","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@codemill-solutions/twinfield-mcp","version":"0.3.0","keywords":["mcp","model-context-protocol","twinfield","accounting","boekhouden","soap","ai","agent","anthropic","claude"],"author":{"name":"CodeMill Solutions B.V."},"license":"MIT","_id":"@codemill-solutions/twinfield-mcp@0.3.0","maintainers":[{"name":"codemillsolutions","email":"dev@codemill.dev"}],"homepage":"https://github.com/CodeMill-Solutions/twinfield-mcp#readme","bugs":{"url":"https://github.com/CodeMill-Solutions/twinfield-mcp/issues"},"dist":{"shasum":"8b41a25e22c8f4a6e4c663d15bc16e9b989a74d8","tarball":"https://registry.npmjs.org/@codemill-solutions/twinfield-mcp/-/twinfield-mcp-0.3.0.tgz","fileCount":27,"integrity":"sha512-75BeBxFcRH9tz1I7cumlyH4QV489RCAVRKIkwQXE6cHm0+vyRnt5WVav+yx+oIwpspwJmUSJHldeEOvpOS7I+Q==","signatures":[{"sig":"MEQCIHLVQsWfue0YsCAFDgh1Eyjz7ZDkik+hr8cbxUDXZKoFAiBtF3pIEXPP6zgEpZyqiTZGQ87DuxzUceOXqOZw9nlLuw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":175272},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"0a4ef630bc2abe3bc0484e9abf6df49775636c11","scripts":{"dev":"tsx src/index.ts","agent":"npx tsx scripts/test-agent.ts","build":"tsc","start":"node dist/index.js","inspect":"npx @modelcontextprotocol/inspector node dist/index.js","authorize":"npx tsx scripts/authorize.ts","prepublishOnly":"npm run build"},"_npmUser":{"name":"codemillsolutions","email":"dev@codemill.dev"},"repository":{"url":"git+https://github.com/CodeMill-Solutions/twinfield-mcp.git","type":"git"},"_npmVersion":"10.8.2","description":"MCP server for the Twinfield accounting SOAP API","directories":{},"_nodeVersion":"20.20.2","dependencies":{"zod":"^3.25.76","axios":"^1.16.1","dotenv":"^16.6.1","fast-xml-parser":"^5.8.0","@anthropic-ai/sdk":"^0.78.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.3","prettier":"^3.8.3","typescript":"^5.9.3","@types/node":"^22.19.19"},"_npmOperationalInternal":{"tmp":"tmp/twinfield-mcp_0.3.0_1779905139165_0.17612205351090782","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@codemill-solutions/twinfield-mcp","version":"0.4.0","description":"MCP server for the Twinfield accounting SOAP API","license":"MIT","author":{"name":"CodeMill Solutions B.V."},"keywords":["mcp","model-context-protocol","twinfield","accounting","boekhouden","soap","ai","agent","anthropic","claude"],"engines":{"node":">=20"},"repository":{"type":"git","url":"git+https://github.com/CodeMill-Solutions/twinfield-mcp.git"},"homepage":"https://github.com/CodeMill-Solutions/twinfield-mcp#readme","bugs":{"url":"https://github.com/CodeMill-Solutions/twinfield-mcp/issues"},"type":"module","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc","start":"node dist/index.js","dev":"tsx src/index.ts","inspect":"npx @modelcontextprotocol/inspector node dist/index.js","authorize":"npx tsx scripts/authorize.ts","agent":"npx tsx scripts/test-agent.ts","prepublishOnly":"npm run build"},"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"dependencies":{"@anthropic-ai/sdk":"^0.78.0","@modelcontextprotocol/sdk":"^1.29.0","axios":"^1.16.1","dotenv":"^16.6.1","fast-xml-parser":"^5.8.0","zod":"^3.25.76"},"devDependencies":{"@types/node":"^22.19.19","prettier":"^3.8.3","tsx":"^4.22.3","typescript":"^5.9.3"},"_id":"@codemill-solutions/twinfield-mcp@0.4.0","gitHead":"c359be05c3621bec23625baa503b9aa80931dd87","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-iHtL2x6yyLNyA5NjnhEKo/wK9c3B+HoLr95degrg/MRS9F5lYKzT0ZnZPqMB54CeExQiCMCUb68sVszWIhLMiQ==","shasum":"427aac53187ae8fab33c5edff3cf85a7b3a79dcc","tarball":"https://registry.npmjs.org/@codemill-solutions/twinfield-mcp/-/twinfield-mcp-0.4.0.tgz","fileCount":27,"unpackedSize":205012,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHkGfl0dsdRLPu9rUNbIN5MSPIkSAQGMKts2fuQt+OnKAiB+vkUtOSLqLh/Nts8iu4RLdIiYeGg3Bq45yCe2ar5eDg=="}]},"_npmUser":{"name":"codemillsolutions","email":"dev@codemill.dev"},"directories":{},"maintainers":[{"name":"codemillsolutions","email":"dev@codemill.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/twinfield-mcp_0.4.0_1779952357297_0.2652408094126215"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-27T12:16:12.685Z","modified":"2026-05-28T07:12:37.583Z","0.1.0":"2026-05-27T12:16:13.067Z","0.1.1":"2026-05-27T12:21:59.801Z","0.2.0":"2026-05-27T13:18:17.329Z","0.3.0":"2026-05-27T18:05:39.309Z","0.4.0":"2026-05-28T07:12:37.438Z"},"bugs":{"url":"https://github.com/CodeMill-Solutions/twinfield-mcp/issues"},"author":{"name":"CodeMill Solutions B.V."},"license":"MIT","homepage":"https://github.com/CodeMill-Solutions/twinfield-mcp#readme","keywords":["mcp","model-context-protocol","twinfield","accounting","boekhouden","soap","ai","agent","anthropic","claude"],"repository":{"type":"git","url":"git+https://github.com/CodeMill-Solutions/twinfield-mcp.git"},"description":"MCP server for the Twinfield accounting SOAP API","maintainers":[{"name":"codemillsolutions","email":"dev@codemill.dev"}],"readme":"# twinfield-mcp\n\nA [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that connects AI agents to [Twinfield](https://www.twinfield.com) accounting via Twinfield's SOAP API.\n\nBuilt with Node.js, TypeScript, and [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk).\n\n> **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+.\n\n---\n\n## Installation\n\n```bash\nnpm install @codemill-solutions/twinfield-mcp\n```\n\nThen add it to your MCP host configuration (e.g. `claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"twinfield\": {\n      \"command\": \"node\",\n      \"args\": [\"node_modules/@codemill-solutions/twinfield-mcp/dist/index.js\"],\n      \"env\": {\n        \"TWINFIELD_OFFICE_CODE\": \"your-office-code\"\n      }\n    }\n  }\n}\n```\n\nThe actual OAuth2 credentials (client id, client secret, 25-year refresh token) live in `~/.twinfield/credentials.json` rather than environment variables — see [Setup](#setup) below.\n\n---\n\n## Prerequisites\n\n- Node.js 20+\n- A Twinfield account with API access enabled\n- An OpenID Connect client registered via the [Twinfield Developer Portal](https://developers.twinfield.com)\n  - **Authorization flow:** authorization code\n  - **Access token type:** JWT\n  - **Redirect URL:** `http://localhost:8765/callback`\n  - **Scopes that will be requested:** `openid twf.user twf.organisation twf.organisationUser offline_access`\n\n---\n\n## Setup\n\n### 1. Install dependencies\n\n```bash\nnpm install\n```\n\n### 2. Run the one-time authorization\n\n```bash\nnpm run authorize\n```\n\nThe interactive script:\n\n1. Asks for `client_id`, `client_secret`, and the office code (CompanyCode) you want to associate.\n2. Opens your browser to the Twinfield login page on `https://login.twinfield.com`.\n3. Receives the authorization code on `http://localhost:8765/callback`.\n4. Exchanges the code for an access + refresh token.\n5. Calls Twinfield's access-token-validation endpoint to discover the per-account cluster URL.\n6. Writes `~/.twinfield/credentials.json` (mode 0600) with the office entry.\n\nTwinfield 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).\n\n### 3. Build\n\n```bash\nnpm run build\n```\n\n### 4. Connect to an MCP host\n\n```json\n{\n  \"mcpServers\": {\n    \"twinfield\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/to/twinfield-mcp/dist/index.js\"],\n      \"env\": {\n        \"TWINFIELD_OFFICE_CODE\": \"your-office-code\"\n      }\n    }\n  }\n}\n```\n\n---\n\n## Multi-office support\n\nA 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.\n\nIf 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.\n\n### Credentials file format\n\n```json\n{\n  \"OFFICE_CODE_A\": {\n    \"clientId\": \"...\",\n    \"clientSecret\": \"...\",\n    \"refreshToken\": \"...\"\n  },\n  \"OFFICE_CODE_B\": {\n    \"clientId\": \"...\",\n    \"clientSecret\": \"...\",\n    \"refreshToken\": \"...\"\n  }\n}\n```\n\nThe file should be `chmod 600` — it contains long-lived refresh tokens. `npm run authorize` sets this automatically when it writes the file.\n\n### Path resolution (first match wins)\n\n| Priority | Path |\n|----------|------|\n| 1 | `TWINFIELD_CREDENTIALS_FILE` environment variable (explicit path) |\n| 2 | `~/.twinfield/credentials.json` (default user-level location) |\n| 3 | `./credentials.json` (local fallback for development) |\n\n### Reloading credentials at runtime\n\nWhen 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.\n\n---\n\n## Available tools (18)\n\n### Authentication & setup\n\n| Tool | Description |\n|------|-------------|\n| `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. |\n| `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. |\n\n### Offices\n\n| Tool | Description |\n|------|-------------|\n| `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. |\n| `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`. |\n\n### Dimensions (master data)\n\nTwinfield 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.\n\n| Tool | Dimtype | Description |\n|------|---------|-------------|\n| `get_customers` | DEB | List all customers (debtors) for an office. |\n| `get_suppliers` | CRD | List all suppliers / vendors (creditors) for an office. |\n| `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. |\n| `get_cost_centers` | KPL | List all cost centres for an office. |\n| `get_projects` | PRJ | List all projects for an office. |\n\nAll dimension tools return an array of `{ code, name?, shortname?, glType? }` entries.\n\n### Transactions (browse queries)\n\nBuilt 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.\n\n| Tool | Default daybook | Description |\n|------|-----------------|-------------|\n| `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. |\n| `get_sales_invoices` | `VRK` | Sales invoice lines. Pass `openOnly=true` to keep only unpaid lines. |\n| `get_purchase_invoices` | `INK` | Purchase invoice lines. Pass `openOnly=true` to keep only unpaid lines. |\n\n**Common parameters** for all three:\n\n- `office?: string` — override the default office.\n- `daybook?: string` — Twinfield daybook code (`VRK`, `INK`, `BNK`, `KAS`, `MEMO`, …). Overrides the per-tool default.\n- `yearperiodFrom?: string`, `yearperiodTo?: string` — inclusive range in `YYYY/PP` format (e.g. `2024/01` to `2024/12`). Must be supplied together.\n- `counterparty?: string` — filter to a single customer/supplier code.\n- `openOnly?: boolean` — client-side post-filter that keeps only rows whose match status is `available` (only on `get_sales_invoices` / `get_purchase_invoices`).\n\n> **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.\n\n### Write tools — master data\n\n| Tool | Description |\n|------|-------------|\n| `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. |\n| `upsert_supplier` | Create or update a supplier (Twinfield dimension type `CRD`). Same shape as `upsert_customer`. |\n| `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. |\n\n### Write tools — transactions\n\n| Tool | Description |\n|------|-------------|\n| `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. |\n| `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. |\n| `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`). |\n\nAll 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.\n\n> **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.\n\n> **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.\n\n---\n\n## Testing\n\n### MCP Inspector (tool-level, no LLM)\n\n```bash\nnpm run inspect\n```\n\nOpens a browser UI where you can call individual tools and inspect raw responses.\n\n### Standalone probes\n\nFor quick command-line validation without the MCP layer:\n\n```bash\nnpx tsx scripts/whoami.ts            # exercises refresh + cluster + userinfo\nnpx tsx scripts/list-offices.ts      # exercises the ProcessXml SOAP path\n```\n\n---\n\n## Architecture\n\n```\nsrc/\n├── index.ts                  # Entry point — loads env + credentials, registers tools, starts stdio transport\n├── twinfield-client.ts       # OAuth2 token cache, cluster discovery, SOAP envelope, ProcessXml call, fair-use handling\n└── tools/\n    ├── auth.ts               # whoami, reload_credentials\n    ├── offices.ts            # list_offices\n    ├── dimensions.ts         # get_customers, get_suppliers, get_gl_accounts,\n    │                         # get_cost_centers, get_projects\n    └── transactions.ts       # get_transactions, get_sales_invoices,\n                              # get_purchase_invoices\n\nscripts/\n├── authorize.ts              # One-time interactive OAuth2 authorization-code flow\n├── whoami.ts                 # Standalone auth-chain probe\n└── list-offices.ts           # Standalone ProcessXml probe\n```\n\n### Auth flow\n\nTwinfield uses OpenID Connect (authorization code + refresh token). The server-side flow:\n\n1. `npm run authorize` runs the **authorization code** grant once per office, captures the refresh token, and writes it to `~/.twinfield/credentials.json`.\n2. 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.\n3. 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.\n4. Every business call goes to `{cluster}/webservices/processxml.asmx` with a SOAP header containing `AccessToken` + `CompanyCode` + `CompanyId xsi:nil=\"true\"`.\n\n### ProcessXml envelope\n\nTwinfield'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.\n\nThe SOAP header is the OAuth2 variant of Twinfield's legacy session-based header:\n\n```xml\n<soap:Header>\n  <Header xmlns=\"http://www.twinfield.com/\">\n    <AccessToken>...</AccessToken>\n    <CompanyCode>YOUR-OFFICE-CODE</CompanyCode>\n    <CompanyId xsi:nil=\"true\" />\n  </Header>\n</soap:Header>\n```\n\n`CompanyId` is `minOccurs=\"1\"` in the WSDL but `nillable=\"true\"` — leaving it out causes a generic HTTP 400 with no SOAP fault body.\n\n---\n\n## Rate limits\n\nTwinfield enforces a credit-based fair-use policy (HTTP 429 with `Retry-After` when exceeded):\n\n| Bucket | Certified clients | Uncertified clients |\n|---|---|---|\n| Per ClientId | 1000 credits/min | **50 credits/min** |\n| Per ClientId + Organisation | 500 credits/min | **25 credits/min** |\n| Per IP | 1000 credits/min | 1000 credits/min |\n\nQuery 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).\n\nA 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.\n\n---\n\n## Troubleshooting\n\n| Error | Likely cause |\n|-------|-------------|\n| `Twinfield OAuth error during refresh token exchange — invalid_grant` | Refresh token was invalidated — re-run `npm run authorize` for the affected office. |\n| `Twinfield token-validation response did not include a usable twf.clusterUrl claim` | Access token is missing the `twf.organisation` scope — re-authorize. |\n| `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`. |\n| `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>`. |\n| `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. |\n| `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. |\n| 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. |\n\n---\n\n## About CodeMill Solutions\n\n[CodeMill Solutions](https://codemill.dev/en/) 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.\n\nOur services include:\n\n- **Custom applications** — portals, dashboards, business software, and fully tailored platforms that truly add value.\n- **API integrations** — connecting your application with other systems and external platforms via smart API connections.\n- **Mobile apps** — iOS and Android apps as a logical extension of your web application(s).\n\n`twinfield-mcp` is one of our open-source integrations, making Twinfield's accounting platform accessible to AI agents through the Model Context Protocol.\n\n📧 [info@codemill.dev](mailto:info@codemill.dev)\n🌐 [codemill.dev](https://codemill.dev/en/)\n💼 [LinkedIn](https://www.linkedin.com/company/codemill-solutions/)\n🐙 [GitHub](https://github.com/CodeMill-Solutions)\n\n---\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}