Projekt
Contributing to mcp-server-sevdesk
Diese Seite gibt es nur auf Englisch.
Auf dieser Seite
Thanks for helping build the best sevDesk MCP server. This guide gets you from clone to merged PR.
Setup#
Requirements: Node 24 (see .nvmrc; Node ≥ 22.12 works) and pnpm via corepack.
corepack enablepnpm install # also wires the git hooks (core.hooksPath = githooks)pnpm check # everything CI runs: lint, format, typecheck, tests + coverage, knip, build, publintUseful scripts: pnpm test:watch, pnpm dev (run the server from source over stdio),
pnpm inspect (open the MCP Inspector against
the built server).
You do not need a sevDesk account to contribute: the test suite runs against recorded and spec-derived fixtures. If you have one, see Live tests.
Branches and pull requests#
- Branch off
mainand open your PR againstmain. - PRs are squash-merged, so the PR title must be a
Conventional Commit:
type(scope): subjectwith a lowercase subject. Types:feat fix chore docs refactor test perf style ci build revert; breaking changes use!(e.g.feat(invoices)!: …). - Local commits are checked by the
commit-msghook; staged files are linted and formatted on commit. - Keep PRs focused. Add or update tests for every behavior change.
Architecture in one minute#
Read docs/architecture.md first. The short version:
src/sevdesk/talks HTTP to sevDesk (auth, pacing, envelope unwrapping, errors).src/domain/holds normalized types, money/date handling and business rules.src/tools/<toolset>/defines MCP tools withdefineTool. Tools never call HTTP directly.src/safety/holds tiers, confirmations, dry-run, audit log, redaction, the OS keychain and the file access tools use for local folders and remote uploads.src/prompts/holds the MCP prompts, rendered from the registered tools.src/http/(withoauth/) andsrc/accounts/hold the HTTP mode: hardening, bearer and OAuth, the approval inbox, and the per-account contexts the tools run with. Tools never see them; they get aConfigand a client as over stdio.
Hand-written zod schemas are the source of truth for sevDesk payloads, because the official OpenAPI spec is incomplete and wrong in places. Every known deviation is listed in spec/deviations.md and has a test.
Adding a tool#
- Pick the toolset (
src/tools/<toolset>/) and the tier:read,write(reversible),commit(legally binding or irreversible) orexternal(reaches third parties). When in doubt, pick the higher tier. - Define it with
defineTool: name, title, description (mention German terms such as Rechnung, Beleg), input/output zod schemas, and all four annotations (readOnlyHint,destructiveHint,idempotentHint,openWorldHint). The metadata lint test fails otherwise. - Implement the handler through the domain layer. Return compact DTOs, never raw sevDesk payloads.
- Add fixtures and tests: happy path, error paths, and tier gating. Update the
tools/listsnapshot (pnpm test -u) and review the diff. - Regenerate the tool reference with
pnpm docs:toolsand commitdocs/tools.md(a test fails when it is stale).
New tools appear in the Claude Desktop extension's manifest by themselves; the build generates its tool list.
Adding a prompt#
Prompts are bookkeeping workflows the user picks in the client. They render text only and never call sevDesk.
- Add the prompt to
src/prompts/catalog.tswithdefinePrompt: a snake_casenamewithout asevdesk_prefix (clients namespace prompts per server), title, description, the tools itrequiresto be listed, and its steps. Keep it in line with the matching workflow inplugin/skills/sevdesk-bookkeeping/references/workflows.md. - Each step lists every tool its text names (in backticks) in
tools; the step is shown only when all of them are registered. Give afallback(what is skipped, naming no tool) when the user should learn that a setting would enable the step; the renderer adds which one. - Arguments are optional strings with a pattern or length check. Show a raw argument only
through
fenceInline, never in the trusted text. - Run
test/unit/prompts/andtest/integration/prompts-list.test.ts: the lint renders every prompt in many configurations and fails when a step names a tool that configuration does not register. Update theprompts/listsnapshot (pnpm test test/integration/prompts-list.test.ts -u) and review the diff, thenpnpm docs:tools.
Compatibility#
From 1.0 the tool names, arguments, output fields, toolsets, tiers, prompts, environment variables, commands, routes, resource URIs, audit-log fields and the extension's setting keys are a public contract (docs/compatibility.md).
- Adding is fine: new tools, optional arguments, output fields, variables.
- Renaming or removing needs a deprecation first: keep the old name working next to the new one,
mark it
Deprecated: use … instead.in its description, and list it in the PR. Removal happens in the next major release. - A PR that breaks the contract anyway marks its title with
!(feat(invoices)!: …) and explains the break in its body. - Descriptions, result texts, error messages and logs are not part of the contract; improve them freely, but re-record the evals when you change what models read (below).
HTTP mode#
The remote mode (--http) is documented for operators in docs/remote.md. To run
it locally in bearer mode against a trial account:
pnpm buildexport SEVDESK_HTTP_AUTH=bearerexport SEVDESK_HTTP_BEARER_TOKEN="$(node dist/index.js generate-secret)"export SEVDESK_API_TOKEN_FILE=~/.config/sevdesk-trial.tokennode dist/index.js --http # or from source: pnpm dev --httpIt listens on http://127.0.0.1:8080/mcp (SEVDESK_HTTP_PORT changes the port). Connect a client
with the bearer header, for example Claude Code:
claude mcp add --transport http sevdesk-dev http://127.0.0.1:8080/mcp \ --header "Authorization: Bearer $SEVDESK_HTTP_BEARER_TOKEN"Pass the flag as pnpm dev --http, not pnpm dev -- --http: pnpm hands the -- on to the
server, which refuses unknown arguments. Plain http on loopback is for development only: browsers
share cookies across the ports of a host, so another local web server could receive the approval
inbox's session cookie. Try the inbox in a browser profile you use for nothing else.
OAuth mode works locally too, with a loopback public URL
(SEVDESK_HTTP_PUBLIC_URL=http://localhost:8080, SEVDESK_HTTP_SECRET, SEVDESK_OAUTH_ACCOUNTS);
claude.ai cannot reach it, so the end-to-end check with claude.ai runs through a tunnel
(docs/recording-session.md).
- HTTP smoke test:
pnpm build && node scripts/smoke-http.ts --bin dist/index.jsstarts the built server in bearer mode on a free port (with a dummy sevDesk token, so nothing reaches sevDesk) and checks the health endpoint, the 401s, the Host and Origin refusals, the MCP handshake on both protocol eras and a clean shutdown. Against a running server:node scripts/smoke-http.ts --url http://127.0.0.1:8080 --bearer "$SEVDESK_HTTP_BEARER_TOKEN". - The image:
docker build -t mcp-server-sevdesk:dev ., then run it read-only as in the Docker quick start and point the smoke test at it with--url. CI does the same in itsdockerjob. - Tests: unit tests in
test/unit/http/(OAuth intest/unit/http/oauth/), integration tests on a real socket intest/integration/http-*.test.ts(bearer, OAuth with the real SDK client, the full flow through the approval inbox) andtest/integration/cross-account.test.ts; helpers for the consent and inbox pages intest/helpers/. Every module with secrets has a test with a captured logger or response that asserts no secret appears; keep it that way.
The Claude Desktop extension#
pnpm mcpb builds the .mcpb bundle into build/: it bundles the server with all its
dependencies into one file (tsdown.mcpb.config.ts), generates the manifest's tool list, writes
the license notices of every bundled package, validates and packs the bundle with the mcpb
library, and then starts the unpacked server the way Claude Desktop does. The CI job mcpb runs
it on every pull request and uploads the bundle as an artifact; the release job publish-mcpb
attaches it to the GitHub release with a provenance attestation.
Change the extension's settings only in mcpb/manifest.json. Keep its JSON.stringify layout
(release-please rewrites the file when it bumps the version, so Prettier ignores it), and give
every optional setting a default: Claude Desktop passes an empty setting without one as the
literal ${user_config.…}, and the build refuses such a manifest. The setting keys are part of the
compatibility contract, because Claude Desktop stores the user's values under
them. To try a build, open build/mcp-server-sevdesk-<version>.mcpb in Claude Desktop.
Keychain and profiles#
The keychain code (src/safety/keychain.ts) takes an injected spawn, so tests never run
security or secret-tool; the profile code takes an injected platform, user id and home folder. A
real round trip is a manual check: token set, token check and token delete with a throwaway
label, then start the server with SEVDESK_API_TOKEN_KEYCHAIN set to it.
Fixtures#
Fixtures live in test/fixtures/ and record their provenance (spec, community or recorded,
plus date and bookkeeping system version). Recorded fixtures must be scrubbed of personal data and
tokens before committing; the recorder does this, but review the diff anyway.
Live tests#
sevDesk has no sandbox: every write is real, and finalized documents cannot be deleted. The live
tooling therefore reads only a short list of variables (SEVDESK_API_TOKEN or
SEVDESK_API_TOKEN_FILE, SEVDESK_BASE_URL, SEVDESK_RATE_LIMIT, SEVDESK_TIMEOUT_MS) and
ignores tier switches from your shell.
pnpm test:livecalls every read tool through the real server against your account, in read-only mode (the HTTP client refuses any data-changing request), and checks that each answers without an error and with schema-valid output. Without a token every test is skipped with the reason. It runs from its own config (vitest.live.config.ts) and never inpnpm test.SEVDESK_LIVE_LIFECYCLE=1 SEVDESK_LIVE_I_UNDERSTAND_WRITES=1 pnpm test:liveadds a write scenario through the write tools (contact, draft, finalize, payments, reminder, cancellation, credit note). Trial accounts only: it refuses accounts that already hold many records, e-mails nothing unlessSEVDESK_LIVE_EMAIL_TOnames your own address, tags what it creates (MCP-E2E <run id>) and prints a cleanup list.pnpm probe --out report.mdchecks the open questions ofspec/deviations.mdagainst the API and reports each finding with scrubbed evidence;--allow-writes(plusSEVDESK_LIVE_I_UNDERSTAND_WRITES=1) adds probes that create their own tagged test data;--probe-email <your address>adds one e-mail probe to that address only.pnpm probe --listshows every probe.pnpm fixtures:recordrecords scrubbed read responses as fixtures (read-only;--dry-runlists the requests).
The step-by-step runbook for a recording session on a free trial account, including what to commit
and what never to commit, is docs/recording-session.md. The manual
workflow Live E2E runs pnpm test:live in CI behind the protected live environment.
Tool-selection evals#
test/evals/ checks which tool a model picks first for about 70 German and English requests.
pnpm evals replays recorded model answers (no API key, no cost); cases without a recording are
skipped as "stale cassette". A cassette is keyed on the whole request, which contains every tool
of its configuration: changing any tool's name, description or schema, or the server
instructions, stales every case of that configuration. Re-record with
ANTHROPIC_API_KEY=… pnpm evals:record (about $1–2 per full run) and commit the cassettes; when
you rename a tool, also update the expected tool names in test/evals/cases.ts. On pull requests that touch tools, .github/workflows/evals.yml replays them and, for
branches of this repository with the evals environment configured, records fresh answers. The
evals are informative, not a required check. Details: test/evals/README.md.
Reporting bugs and security issues#
Use the issue forms for bugs and feature requests. Report vulnerabilities privately, see SECURITY.md.
License#
By contributing you agree that your contributions are licensed under the MIT License.