MCP server for the Lexware Office API. Manage invoices, contacts, articles, vouchers, and more through the Model Context Protocol.
Unofficial — community project. Not affiliated with, endorsed by, or supported by Lexware GmbH or Haufe Group. "Lexware" and "Lexware Office" are trademarks of their respective owners; used here only to identify the API this client targets (nominative fair use).
66 tools across 20 resource domains, with 6 entry points so you can pick the right server for your MCP client's tool limit.
npm install -g @lazyants/lexware-mcp-serverOr run directly:
npx @lazyants/lexware-mcp-serverThe API token is resolved in this order:
- OS keyring (recommended — token never written to disk in plain text)
- Environment variable
LEXWARE_API_TOKEN
Get your token from the Lexware Office API settings, then store it with the native credential manager for your OS.
Important
The commands below read the token from an interactive prompt rather than taking it as an argument, so it never lands in your shell history or the process list. Avoid pasting the token directly onto the command line.
Omitting the value after -w makes security prompt for the token (with confirmation):
security add-generic-password -s "lexware-mcp" -a "api-token" -wcmdkey can only take the token as a command-line argument, which exposes it in
the process list. Instead, read it from a hidden prompt and write it straight
into Windows Credential Manager via CredWrite, so the token never reaches argv.
The credential's target name is <account>.<service> — api-token.lexware-mcp
for the default service — which is exactly what the server reads back:
$secure = Read-Host -AsSecureString "Lexware API token"
Add-Type -Namespace LexwareKeyring -Name Native -MemberDefinition @'
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CREDENTIAL {
public uint Flags;
public uint Type;
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public uint CredentialBlobSize;
public IntPtr CredentialBlob;
public uint Persist;
public uint AttributeCount;
public IntPtr Attributes;
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredWriteW(ref CREDENTIAL credential, uint flags);
'@
$blob = [Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($secure)
try {
$cred = New-Object LexwareKeyring.Native+CREDENTIAL
$cred.Type = 1 # CRED_TYPE_GENERIC
$cred.Persist = 2 # CRED_PERSIST_LOCAL_MACHINE
$cred.TargetName = 'api-token.lexware-mcp' # "<account>.<service>"
$cred.UserName = 'api-token'
$cred.CredentialBlob = $blob
$cred.CredentialBlobSize = $secure.Length * 2 # UTF-16 bytes, no terminator
if (-not [LexwareKeyring.Native]::CredWriteW([ref]$cred, 0)) {
throw "CredWrite failed (Win32 error $([Runtime.InteropServices.Marshal]::GetLastWin32Error()))"
}
Write-Host 'Stored Lexware API token in Windows Credential Manager.'
} finally {
[Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($blob)
$secure.Dispose()
Remove-Variable secure, blob
}Using a custom
LEXWARE_KEYRING_SERVICE(e.g.acme)? SetTargetNametoapi-token.acmeto match — the server looks the token up under<account>.<service>.
secret-tool store --label="Lexware Office API" service lexware-mcp username api-token
# (prompts for the token value)Once stored, MCP config files need no credentials at all — the server reads the token from the keyring at startup.
If you prefer not to use the keyring, set LEXWARE_API_TOKEN in your shell or MCP client config:
export LEXWARE_API_TOKEN=your-token-here| Variable | Default | Description |
|---|---|---|
LEXWARE_API_TOKEN |
— | API token; used when the keyring has no entry for the configured service |
LEXWARE_KEYRING_SERVICE |
lexware-mcp |
Keyring service name. Override when connecting to multiple Lexware accounts simultaneously — run one server instance per account, each with its own service name |
Optionally override the webhook-signature public key used by lexware_verify_webhook_signature
(by default fetched from Lexware and cached):
export LEXWARE_WEBHOOK_PUBLIC_KEY="$(cat lexware-webhook-public.pem)"| Command | Domains | Tools |
|---|---|---|
lexware-mcp-server |
All 20 domains | 66 |
lexware-mcp-sales |
Invoices, Credit Notes, Quotations, Order Confirmations, Delivery Notes, Down Payment Invoices, Dunnings, Voucherlist | 32 |
lexware-mcp-contacts |
Contacts, Articles | 10 |
lexware-mcp-bookkeeping |
Vouchers, Voucherlist, Payments | 8 |
lexware-mcp-reference |
Countries, Payment Conditions, Posting Categories, Profile, Print Layouts | 5 |
lexware-mcp-system |
Event Subscriptions, Files, Recurring Templates | 12 |
Use split servers to reduce context size — pick only the splits you need.
Add to ~/.claude/settings.json. If you stored the token in the OS keyring under the default service name lexware-mcp (recommended), no env key is needed:
{
"mcpServers": {
"lexware": {
"command": "npx",
"args": ["-y", "@lazyants/lexware-mcp-server"]
}
}
}If you prefer the environment variable approach:
{
"mcpServers": {
"lexware": {
"command": "npx",
"args": ["-y", "@lazyants/lexware-mcp-server"],
"env": { "LEXWARE_API_TOKEN": "your-token-here" }
}
}
}Use split servers to reduce context size — pick only the entry points you need. The -p @lazyants/lexware-mcp-server flag tells npx which package to source the command from; the final argument (e.g. lexware-mcp-sales) is the specific entry-point binary defined in that package (see Entry Points):
{
"mcpServers": {
"lexware-sales": {
"command": "npx",
"args": ["-y", "-p", "@lazyants/lexware-mcp-server", "lexware-mcp-sales"]
},
"lexware-contacts": {
"command": "npx",
"args": ["-y", "-p", "@lazyants/lexware-mcp-server", "lexware-mcp-contacts"]
}
}
}Multi-account example (two Lexware companies, tokens stored under separate keyring service names):
{
"mcpServers": {
"lexware-company-a": {
"command": "npx",
"args": ["-y", "@lazyants/lexware-mcp-server"],
"env": { "LEXWARE_KEYRING_SERVICE": "lexware-company-a" }
},
"lexware-company-b": {
"command": "npx",
"args": ["-y", "@lazyants/lexware-mcp-server"],
"env": { "LEXWARE_KEYRING_SERVICE": "lexware-company-b" }
}
}
}Add to claude_desktop_config.json. With the OS keyring (recommended — assumes the token is stored under the default service name lexware-mcp):
{
"mcpServers": {
"lexware": {
"command": "npx",
"args": ["-y", "@lazyants/lexware-mcp-server"]
}
}
}With an environment variable instead:
{
"mcpServers": {
"lexware": {
"command": "npx",
"args": ["-y", "@lazyants/lexware-mcp-server"],
"env": { "LEXWARE_API_TOKEN": "your-token-here" }
}
}
}lexware_create_invoice (supports finalize=true at creation), lexware_get_invoice, lexware_download_invoice_file, lexware_pursue_invoice, lexware_deeplink_invoice
lexware_create_credit_note, lexware_get_credit_note, lexware_download_credit_note_file, lexware_pursue_credit_note, lexware_deeplink_credit_note
lexware_create_quotation, lexware_get_quotation, lexware_download_quotation_file, lexware_deeplink_quotation
lexware_create_order_confirmation, lexware_get_order_confirmation, lexware_download_order_confirmation_file, lexware_pursue_order_confirmation, lexware_deeplink_order_confirmation
lexware_create_delivery_note, lexware_get_delivery_note, lexware_download_delivery_note_file, lexware_pursue_delivery_note, lexware_deeplink_delivery_note
lexware_get_down_payment_invoice, lexware_download_down_payment_invoice_file, lexware_deeplink_down_payment_invoice
lexware_get_dunning, lexware_download_dunning_file, lexware_pursue_dunning, lexware_deeplink_dunning
lexware_list_voucherlist
lexware_list_contacts, lexware_get_contact, lexware_create_contact, lexware_update_contact, lexware_deeplink_contact
lexware_list_articles, lexware_get_article, lexware_create_article, lexware_update_article, lexware_delete_article
lexware_list_vouchers, lexware_get_voucher, lexware_create_voucher, lexware_update_voucher, lexware_upload_voucher_file, lexware_deeplink_voucher
lexware_get_payments
lexware_list_countries
lexware_list_payment_conditions
lexware_list_posting_categories
lexware_get_profile
lexware_list_print_layouts
lexware_create_event_subscription, lexware_list_event_subscriptions, lexware_get_event_subscription, lexware_delete_event_subscription, lexware_verify_webhook_signature
lexware_upload_file, lexware_download_file, lexware_get_file_status, lexware_deeplink_file
lexware_list_recurring_templates, lexware_get_recurring_template, lexware_deeplink_recurring_template
- Use the OS keyring to keep your API token out of config files and shell history entirely (see Configuration)
- Never commit your API token to version control
- Use read-only access when you only need to list/get resources
- Create, update, and delete tools modify real business data — invoices, contacts, and accounting records in your Lexware account
- Rate limiting is handled automatically: requests retry with exponential backoff on 429, including file uploads — the multipart body is rebuilt fresh on every retry attempt, so it can be replayed safely
Releases ship via the GitHub Release event. Maintainer flow:
-
Bump the version in
package.json,package-lock.json, andserver.json(npm version <x.y.z> --no-git-tag-versionupdates the first two together).npm run check-versionshard-fails unlesspackage.json#/version,server.json#/packages[0].version, and bothpackage-lock.jsonversion fields (root andpackages[""]) all agree.server.json#/versionis checked more loosely: it must be present, but it is only compared againstpackages[0].versionas a regression check — it may legitimately be ahead (registry-only republishes bump just that field), so a value left behind at the previous release passes with aWARN:line and no failure. For an ordinary release both should move together, so read the script's output rather than trusting its exit code.CHANGELOG.mdis not checked at all. -
Update
CHANGELOG.md. -
Commit, and merge the version bump to
mainbefore creating the release. Then create the tag yourself, on a SHA you have checked, and only then create the release from it:V=X.Y.Z && PR=<release-pr-number> && SHA="$(gh pr view "$PR" --json mergeCommit -q .mergeCommit.oid)" && test -n "$SHA" && git fetch origin main && git merge-base --is-ancestor "$SHA" origin/main && PKG="$(git show "$SHA:package.json")" && test "$(printf '%s' "$PKG" | node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).version')" = "$V" && CL="$(git show "$SHA:CHANGELOG.md")" && printf '%s\n' "$CL" | awk -v v="$V" 'index($0,"## ["v"]")==1{f=1;next} /^## \[/{f=0} /^\[[0-9]+\.[0-9]+\.[0-9]+\]:/{f=0} f' > "/tmp/notes-v$V.md" && grep -q '[^[:space:]]' "/tmp/notes-v$V.md" && git tag -a "v$V" "$SHA" -m "v$V" && git push origin "v$V" && gh release create "v$V" --verify-tag --notes-file "/tmp/notes-v$V.md"
The failure this prevents: with no existing tag,
gh release createplaces one on the tip of the default branch, so running it while the bump is still on a release branch tags the previous release's commit. The workflow then publishes whatever version it finds in that commit'spackage.json, and you get avX.Y.ZGitHub Release that silently republishes the old version. Nothing downstream catches it — neither the workflow norcheck-versionscompares the tag against the version files (tracked in #103), which is why the sequence above has to do it.Each element is load-bearing:
gh pr view … .mergeCommit.oidnames the release PR's own squash commit. Do not substitutegit rev-parse origin/main— that is merely whatever is onmainat the moment you look, so an unrelated merge landing in the gap gets tagged and shipped instead.ghexits 0 and prints nothing for an unmerged PR, hence the explicittest -n.- The
&&chain stops on the first failure instead of falling through to the irreversible step. Bothgit showcalls are assigned to a variable rather than piped directly, so their exit status is actually checked — a pipeline reports only its last command's status unlesspipefailis set, which is not assumed here. git merge-base --is-ancestorproves the commit is actually reachable frommain. Mere existence is not enough — a commit can be present locally because some other branch was fetched, and if its version files happen to match it would otherwise sail through every remaining check.- The version test reads
package.jsonout of the target commit, not the working tree, which would still show the right version while$SHApointed elsewhere. - The
awklifts that version's section out of the commit'sCHANGELOG.mdfor--notes-file. Without it the release body is whatever--notes-from-tagfinds in the annotation — for this flow, the literal stringvX.Y.Z, which is a poor release note for any version and an actively misleading one for a major carrying a breaking change. It stops at the next## [heading or at the first link-reference definition, because the oldest entry in the file has no heading after it and would otherwise swallow the entire link-reference block.grep -qrather thantest -sguards the result: a section that is empty apart from its blank line still produces a one-byte file, whichtest -saccepts. --verify-tagmakesghabort rather than invent a tag if the push did not land — the guard againstghfalling back to the tip-of-default-branch behavior described above.
If
gh release createfails after the tag is already pushed, do not rerun the whole block — it will stop atgit tag, which is correct. Rerun only the final command. -
The
Publish to npm + MCP Registryworkflow runs automatically: itnpm publishes with provenance, polls the registry until the tarball is available, then pushes the matchingserver.jsonto the MCP Registry viamcp-publisher.
The workflow skips npm publish cleanly if the version is already on npm (cutover guard for releases that were partially published manually).
Publishing uses npm Trusted Publishing via OIDC — there is no NPM_TOKEN secret. The workflow's id-token: write permission is exchanged for a short-lived, one-shot publish token at publish time, using the trusted-publisher binding configured for @lazyants/lexware-mcp-server in the npm web UI. The only setup required is that trusted-publisher binding on npm; nothing needs to be stored in repository secrets.
This is an unofficial, independent community project. It is not affiliated with, endorsed by, sponsored by, or supported by Lexware GmbH, Haufe Group, or any of their affiliates. For official Lexware support, contact Lexware directly — issues with this MCP server should be reported here, not to Lexware.
"Lexware" and "Lexware Office" are trademarks of their respective owners and are used in this project's name and documentation under nominative fair use, solely to identify the third-party API this client connects to.
Create, update, and delete operations modify real business data in your Lexware account. The authors provide this software "as-is" and accept no responsibility for unintended changes, data loss, or any other damages arising from its use. Test against a sandbox or non-critical account before running write operations against production data.
FSL-1.1-MIT — see LICENSE for the full terms.