feat(plugin): official Claude Code plugin with marketplace and seven workflow skills (#1088)

Ships an installable Claude Code plugin (/plugin marketplace add
erp-mafia/accounted) that bundles the MCP connection (OAuth, zero-key)
with seven short workflow skills following the Swedish bookkeeping
rhythm: start, bookkeep, check, month-close, vat, payroll, year-end.

Wrappers are deliberately thin: they ground in the agent briefing and
Accounted:// resources, load server-side workflow skills and regulatory
atoms via gnubok_load_skill at need, and stage every write for user
approval. No knowledge is duplicated into the plugin.

A vitest cross-checks every skill slug, atom id, resource URI, and tool
name the wrappers reference against the MCP server source, so a server
rename fails CI instead of a user's chat session.

Assessment and follow-ups in dev_docs/claude_plugin.md (local, dev_docs
is unpublished by design).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-21 12:59:57 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b420f3e1d9
commit e33cc2428d
12 changed files with 353 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
{
"name": "accounted",
"owner": {
"name": "Accounted (erp-mafia)"
},
"description": "Official plugin marketplace for Accounted, the open-source Swedish bookkeeping platform built to be the ledger agents best talk with.",
"plugins": [
{
"name": "accounted",
"source": "./claude-plugin",
"description": "Connect your Accounted ledger and get short, approval-gated workflow skills: daily bookkeeping, health check, month close, VAT (moms), payroll, year-end (bokslut). Pulls the company's live data over MCP and loads Swedish accounting knowledge from the product when needed.",
"category": "business",
"keywords": [
"accounting",
"bookkeeping",
"sweden",
"sverige",
"bokforing",
"moms",
"bokslut",
"accounted",
"mcp"
]
}
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "accounted",
"displayName": "Accounted",
"description": "Official Accounted plugin: Swedish double-entry bookkeeping flows for Claude. Connects your ledger over MCP and ships short workflow skills (daily bookkeeping, health check, month close, VAT, payroll, year-end) that work from the company's live data and load Swedish accounting knowledge from the product when needed. Every write is staged for your approval; nothing is booked on its own.",
"version": "0.1.0",
"author": {
"name": "Accounted (erp-mafia)"
},
"homepage": "https://app.gnubok.se/docs/api/connect-claude",
"repository": "https://github.com/erp-mafia/accounted",
"license": "MIT",
"keywords": [
"accounting",
"bookkeeping",
"sweden",
"sverige",
"bokforing",
"moms",
"bokslut",
"accounted",
"mcp"
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"accounted": {
"type": "http",
"url": "https://app.gnubok.se/api/extensions/ext/mcp-server/mcp"
}
}
}
+45
View File
@@ -0,0 +1,45 @@
# Accounted plugin for Claude Code
The official plugin for [Accounted](https://app.gnubok.se), the open-source Swedish bookkeeping platform. Installing it gives Claude two things at once:
1. **The connection**: the Accounted MCP server (90+ bookkeeping tools, resources, and loadable skills) via OAuth. No API key needed.
2. **The flows**: seven short workflow skills that follow the Swedish bookkeeping rhythm. Each one grounds itself in your company's live data, loads the product's Swedish accounting knowledge when it needs it, and stages every write for your approval. Nothing is ever booked without you saying yes.
## Install
```text
/plugin marketplace add erp-mafia/accounted
/plugin install accounted@accounted
```
Then run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). Start with `/accounted:start`.
## Skills
| Command | What it does |
|---|---|
| `/accounted:start` | Connect, orient, and surface what needs attention |
| `/accounted:bookkeep` | Clear unbooked bank transactions and receipts (daily) |
| `/accounted:check` | Read-only health check with a prioritized fix list |
| `/accounted:month-close` | Close the month against the product's checklist |
| `/accounted:vat` | Prepare and reconcile the momsdeklaration |
| `/accounted:payroll` | Monthly salary run and AGI underlag |
| `/accounted:year-end` | Bokslut, readiness-gated |
The skills are deliberately thin: the deep procedural and regulatory content (month-end checklist, VAT rutor, payroll rules, bokslut law) lives server-side in Accounted and is loaded at need via `gnubok_load_skill`, so it is always in sync with the product and tailored to your company. `gnubok_list_skills` shows everything available.
## How writes work
Every write tool in Accounted stages a **pending operation** with a preview instead of booking directly. Claude shows you the preview; only `gnubok_approve_pending_operation`, after your explicit approval, books it. Period locks and Swedish accounting law (immutable vouchers, balanced entries, sequential voucher numbers) are enforced by the product itself.
## Self-hosted
Point the MCP connection at your own instance instead: remove the bundled server and add your own with `claude mcp add --transport http accounted https://your-host/api/extensions/ext/mcp-server/mcp`, or use the [`gnubok-mcp`](https://www.npmjs.com/package/gnubok-mcp) stdio bridge with a `gnubok_sk_` API key.
## Disclaimer
This plugin is not legal, tax, or audit advice. Output is underlag for you and your accountant. Nothing is filed or sent anywhere automatically.
## License
MIT (the plugin). The Accounted platform itself is AGPL-3.0.
+27
View File
@@ -0,0 +1,27 @@
---
name: bookkeep
description: Clear the bookkeeping inbox, categorize bank transactions and receipts into correct vouchers. Use when the user says "bokfor", "bokfora", "book this", "categorize my transactions", "osorterade transaktioner", "kvitton", or wants daily bookkeeping done.
argument-hint: [period or counterparty]
---
# Bookkeep
Work through unbooked bank transactions and receipts and stage correct vouchers. This is the highest-frequency flow; accuracy comes from the company's own history, not from guessing.
## Flow
1. If not already done this session, call `gnubok_get_agent_briefing` (accounting method changes how income is booked: faktureringsmetoden credits 1510 via invoices, kontantmetoden books on payment).
2. Find what is unbooked: start from `Accounted://attention`, then discover the listing and categorization tools with `gnubok_search_tools` (for example "uncategorized transactions", "categorize", "upload receipt").
3. For each item, decide the posting in this order of authority:
1. Explicit mapping rules the company has configured.
2. Observed history in `Accounted://ledger/context`: how THIS company booked this counterparty before (dominant account, VAT treatment, frequency as evidence). Prefer these over textbook answers, but frequency is not permission to auto-post.
3. Only then general knowledge, and for VAT treatment always load it: `gnubok_load_skill("horizontal/swedish-vat")` for deductibility edge cases (representation caps, EU trade, reverse charge). Never answer Swedish VAT from memory.
4. Stage the categorizations, grouped by counterparty so the user can approve in coherent batches. Present each preview with account, VAT treatment, and why. Approve only what the user confirms, via `gnubok_approve_pending_operation`.
5. If a tool response carries `period_status` locked or closed, stop that item and explain; never work around a period lock.
6. Finish with a short summary in the user's language: booked, skipped and why, what remains, and the next relevant deadline.
## Rules
- Evidence over guesses: ledger context and mapping rules outrank general knowledge.
- Every write stages a pending operation; the user approves before anything is booked.
- Amounts are SEK with ore precision; never round away balance.
+27
View File
@@ -0,0 +1,27 @@
---
name: check
description: Read-only health check of the books. Use when the user asks "hur ligger vi till", "check my books", "ar bokforingen i ordning", "status", "veckokoll", "manadskoll", or wonders whether anything looks wrong or is overdue.
---
# Check
A read-only pass over the books that ends in a short prioritized list. This flow never writes anything; it routes each finding to the flow that fixes it.
## Flow
1. If not already done this session, call `gnubok_get_agent_briefing`.
2. Read `Accounted://attention`, `Accounted://period/active`, and `Accounted://recent-activity`.
3. Assess, in this order:
- Unbooked backlog: how many items, how old is the oldest?
- Unreconciled bank transactions in the active period.
- Overdue customer invoices and unpaid supplier invoices.
- Unapproved pending operations waiting on the user (`gnubok_list_pending_operations`).
- Upcoming deadlines: moms, AGI, F-skatt, bokslut. Use dates from the product, not memorized ones.
- Period status: is a period that should be closed still open?
4. Present a short table in the user's language: finding, severity, and which flow fixes it (`/accounted:bookkeep`, `/accounted:month-close`, `/accounted:vat`, `/accounted:payroll`, `/accounted:year-end`).
5. If everything is clean, say so plainly and state the next deadline.
## Rules
- Strictly read-only: do not stage or approve anything in this flow, even if the fix is obvious. Offer the fixing flow instead.
- Report only what the data shows; no speculative findings.
+25
View File
@@ -0,0 +1,25 @@
---
name: month-close
description: Close a month in Accounted. Use when the user says "stang manaden", "manadsavslut", "month-end close", "close the period", or wants the monthly closing routine done.
argument-hint: [month]
---
# Month close
Run the monthly close as a checklist against live data. The authoritative checklist lives server-side and is tailored to this company; this skill orchestrates it, it does not replace it.
## Flow
1. If not already done this session, call `gnubok_get_agent_briefing` and read `Accounted://period/active` to confirm which period is being closed.
2. Load the checklist: `gnubok_load_skill("month-end-close")`. Follow it step by step with the company's real numbers.
3. Bank first: if unreconciled transactions exist in the period, load `gnubok_load_skill("bank-reconciliation")` and clear them before anything else. Unbooked items route through the `/accounted:bookkeep` flow.
4. Work the remaining checklist items (accruals, recurring vouchers, control balances). Every correction is staged and individually approved by the user; use storno-style corrections through the product's tools, never edit posted entries.
5. If the month ends a VAT period, hand over to `/accounted:vat` rather than improvising the momsdeklaration inside this flow.
6. Lock or close the period only as the final step, only after the user explicitly confirms, as its own staged operation.
7. Report in the user's language: closed, or blocked with a concrete list of what stands in the way.
## Rules
- The server-side checklist is authoritative; do not substitute a generic month-end list from memory.
- Every write stages a pending operation; the user approves each before it is booked.
- Never work around a locked or closed period; surface it and stop.
+24
View File
@@ -0,0 +1,24 @@
---
name: payroll
description: Run monthly Swedish payroll and prepare AGI. Use when the user says "lon", "loner", "lonekorning", "payroll", "arbetsgivardeklaration", "AGI", "arbetsgivaravgifter", or asks about salary bookings, formaner, or sick pay in a payroll context.
argument-hint: [month]
---
# Payroll
Run the monthly salary cycle: verify the salary run, stage the bookings, prepare the arbetsgivardeklaration (AGI). The product computes; the loaded skills explain; this flow never calculates tax from memory.
## Flow
1. If not already done this session, call `gnubok_get_agent_briefing`. If the company has no employees, say so and stop (an enskild firma owner takes eget uttag, not salary; offer to explain via the loaded skill).
2. Load the knowledge: `gnubok_load_skill("payroll-monthly")` (the monthly procedure) and `gnubok_load_skill("horizontal/swedish-payroll")` for rules (skatteavdrag, arbetsgivaravgifter and age reductions, formaner, karensavdrag, semesterloneskuld).
3. Verify the month's salary run in the product: does it exist, is it complete, are one-off items in (bonus, sick days, formaner, utlagg)? Ask the user about anything the data cannot show.
4. Stage the salary bookings from the run and present the preview: gross, skatteavdrag, arbetsgivaravgifter, net, per the 7xxx account mapping the product produces. The user approves via `gnubok_approve_pending_operation`.
5. Prepare the AGI underlag and state the filing deadline (from the product's deadline data, not memory). Filing with Skatteverket is the user's action unless the integration is connected.
6. Report in the user's language: booked, AGI status, deadline, anything pending.
## Rules
- Never compute tax deductions, avgifter, or forman values yourself; use the product's computed run and the loaded skills to sanity-check it.
- Every write stages a pending operation; the user approves before anything is booked.
- Payroll data is sensitive: show individual salaries only when the user asks for that level of detail.
+29
View File
@@ -0,0 +1,29 @@
---
name: start
description: Connect and orient in the user's Accounted bookkeeping. Use on first contact with Accounted in a session, when the user says "kom igang", "get started", "connect my bookkeeping", "vad behover jag gora", or asks what this plugin can do.
---
# Start
Verify the connection, learn who this company is, and surface what needs attention. Run this before any other Accounted flow in a session.
## Flow
1. Call `gnubok_get_agent_briefing`. This is the single source for company facts: entity type (aktiebolag or enskild firma), accounting method (faktureringsmetoden or kontantmetoden), VAT period, employees, and ledger context. Never assume these; the flows below behave differently depending on them.
- If the call fails with an auth error, the MCP server is not connected yet: tell the user to run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). Self-hosted users: see the plugin README.
2. Read `Accounted://attention` and `Accounted://period/active`.
3. Present a short orientation in the user's language: company name and form, active fiscal period and its lock status, and the top 3 items needing attention.
4. Point at the flows, matched to what attention showed:
- `/accounted:bookkeep` - clear unbooked transactions and receipts (daily)
- `/accounted:check` - read-only health check of the books
- `/accounted:month-close` - close the month
- `/accounted:vat` - prepare the momsdeklaration
- `/accounted:payroll` - monthly salary run and AGI
- `/accounted:year-end` - bokslut
5. Mention that deeper, company-tailored guides exist on the server: `gnubok_list_skills` lists them (workflow guides plus Swedish regulatory skills, filtered to this company), and `gnubok_load_skill(slug)` loads any of them.
## Rules
- Ground every statement in the briefing and resources; never guess company facts.
- Swedish accounting or tax questions are answered from loaded skills, never from memory.
- Every write in Accounted stages a pending operation for the user to approve. Nothing is ever booked without explicit approval.
+24
View File
@@ -0,0 +1,24 @@
---
name: vat
description: Prepare and review the Swedish VAT return (momsdeklaration). Use when the user says "moms", "momsdeklaration", "VAT return", "momsrapport", asks about rutor, EU-moms, omvand skattskyldighet, or when a VAT period is ending.
argument-hint: [period]
---
# VAT
Prepare the momsdeklaration underlag from the ledger and reconcile it before anything is booked or filed. VAT is the highest-error area in Swedish bookkeeping: always work from loaded knowledge, never memory.
## Flow
1. If not already done this session, call `gnubok_get_agent_briefing`: it gives VAT registration status, period length (monthly, quarterly, yearly), and accounting method. If the company is not VAT registered, say so and stop.
2. Load the knowledge: `gnubok_load_skill("quarterly-vat-review")` (the review procedure, valid for any period length) and `gnubok_load_skill("horizontal/swedish-vat")` for ruta mapping and edge cases (reverse charge, EU trade, import VAT, representation).
3. Verify the period is fully booked first: any unbooked transactions in the period make the return wrong. Route gaps through `/accounted:bookkeep` before continuing.
4. Generate the VAT report with the product's tools (discover via `gnubok_search_tools`, for example "vat report momsdeklaration") and reconcile: report rutor against the 26xx account balances, and against the previous period for anomalies.
5. Present a per-ruta summary in the user's language, flagging anything unusual with the ledger evidence behind it.
6. Booking the VAT settlement (redovisning mot 2650/1650) is a staged operation the user approves. Filing with Skatteverket is the user's action: prepare the underlag, state the deadline, and where the product's Skatteverket integration is connected, point at it.
## Rules
- Never compute or assert a ruta mapping from memory; it must come from the loaded skills and the product's report.
- Every write stages a pending operation; the user approves before anything is booked.
- Never work around a locked period; a locked VAT period means the correction goes in the current one, per the loaded skill's procedure.
+24
View File
@@ -0,0 +1,24 @@
---
name: year-end
description: Swedish year-end closing (bokslut). Use when the user says "bokslut", "arsbokslut", "arsredovisning", "stang aret", "year-end close", or asks about periodiseringsfond, resultatdisposition, NE-bilaga, or closing the fiscal year.
argument-hint: [fiscal year]
---
# Year-end
Run the bokslut as a readiness-gated, step-by-step flow. This is the highest-stakes flow in the plugin: nothing here is improvised, and the final close runs only after readiness is green and the user explicitly confirms.
## Flow
1. If not already done this session, call `gnubok_get_agent_briefing`: entity type decides the whole shape (aktiebolag: bolagsskatt and resultatdisposition; enskild firma: NE-bilaga, egenavgifter, rantefordelning).
2. Readiness first: call `gnubok_year_end_readiness` (read-only preflight). Every blocker it reports is fixed through the other flows (`/accounted:bookkeep`, `/accounted:month-close`, `/accounted:vat`) before continuing. Do not start closing entries on a year that is not ready.
3. Load the knowledge: `gnubok_load_skill("year-end-close")` (the procedure) and `gnubok_load_skill("horizontal/swedish-year-end-closing")` (the law and the account-level detail). When the user wants to optimize (periodiseringsfond, overavskrivningar), also load `gnubok_load_skill("horizontal/swedish-tax-planning")` and present options with trade-offs, not a single answer.
4. Work the bokslutstransaktioner in the loaded order, one staged operation at a time, each approved by the user with the amounts and accounts visible.
5. The final close (`gnubok_run_year_end`) is high-risk: run it only after readiness is green again and the user has explicitly confirmed in this conversation.
6. Report what remains outside the ledger: arsredovisning and filing for AB (load `gnubok_load_skill("horizontal/swedish-financial-reporting")` if asked), INK2 or NE underlag, deadlines from the product's data.
## Rules
- Readiness gates everything; never bypass a red readiness check.
- Every write stages a pending operation; the user approves each before it is booked.
- Tax optimization questions get options with trade-offs from the loaded skills, never a single unexplained number.
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { workflowSkills } from '../skills'
import { dataResources } from '../resources'
import { discoverAtoms } from '@/scripts/lib/atom-discovery'
/**
* The Claude Code plugin (claude-plugin/) ships thin wrapper skills whose value
* is that every slug, resource URI, and tool name they mention resolves against
* this MCP server. A rename on the server side must fail here, not in a user's
* chat session.
*/
const repoRoot = join(__dirname, '..', '..', '..', '..')
const skillsDir = join(repoRoot, 'claude-plugin', 'skills')
function readPluginSkills(): { file: string; body: string }[] {
return readdirSync(skillsDir).map((dir) => {
const file = join(skillsDir, dir, 'SKILL.md')
return { file: `${dir}/SKILL.md`, body: readFileSync(file, 'utf8') }
})
}
const pluginSkills = readPluginSkills()
const serverSource = readFileSync(join(__dirname, '..', 'server.ts'), 'utf8')
describe('claude-plugin wrapper references', () => {
it('ships the seven v1 skills', () => {
expect(pluginSkills.map((s) => s.file).sort()).toEqual([
'bookkeep/SKILL.md',
'check/SKILL.md',
'month-close/SKILL.md',
'payroll/SKILL.md',
'start/SKILL.md',
'vat/SKILL.md',
'year-end/SKILL.md',
])
})
it('every gnubok_load_skill slug resolves to a workflow skill or a registry atom', async () => {
const workflowSlugs = new Set(workflowSkills.map((s) => s.slug))
const atomIds = new Set((await discoverAtoms(repoRoot)).map((a) => a.id))
for (const { file, body } of pluginSkills) {
for (const [, slug] of body.matchAll(/gnubok_load_skill\("([^"]+)"\)/g)) {
const known = workflowSlugs.has(slug) || atomIds.has(slug)
expect(known, `${file} references unknown skill slug "${slug}"`).toBe(true)
}
}
})
it('every Accounted:// resource URI exists on the server', () => {
const uris = new Set(dataResources.map((r) => r.uri))
for (const { file, body } of pluginSkills) {
for (const [uri] of body.matchAll(/Accounted:\/\/[a-z/-]+/g)) {
expect(uris.has(uri), `${file} references unknown resource "${uri}"`).toBe(true)
}
}
})
it('every gnubok_* tool name exists on the server', () => {
for (const { file, body } of pluginSkills) {
for (const [tool] of body.matchAll(/gnubok_[a-z_]+/g)) {
expect(
serverSource.includes(`name: '${tool}'`),
`${file} references unknown tool "${tool}"`,
).toBe(true)
}
}
})
})