From 159823583c92dde5480a05b9707dec1d3c51f7be Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Tue, 25 Aug 2026 18:39:01 +0200 Subject: [PATCH] feat(plugin): /accounted:setup command, CONNECTORS.md, v1.1.0 (#1902) The Claude plugin is the one-click install for Cowork and Claude Code, so it should also be the entry to agent-first onboarding (#1814). /accounted:setup connects the bundled connector (creating the account on the sign-in screen if needed), hands off to the server-side onboarding skill when the account has no company, then the bank and Skatteverket links. CONNECTORS.md documents the single bundled connector the way Anthropic's own plugins do. Version 1.1.0 so marketplaces that sync on version bumps pick it up. The plugin-refs guard now also validates commands/*.md against the server. Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- DECISIONS.md | 1 + claude-plugin/.claude-plugin/plugin.json | 2 +- claude-plugin/CONNECTORS.md | 18 ++++++++++ claude-plugin/README.md | 3 +- claude-plugin/commands/setup.md | 33 +++++++++++++++++++ .../__tests__/claude-plugin-refs.test.ts | 15 +++++++-- 6 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 claude-plugin/CONNECTORS.md create mode 100644 claude-plugin/commands/setup.md diff --git a/DECISIONS.md b/DECISIONS.md index 0b57e4b0..768c33e0 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1235,3 +1235,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-25] Proposal line-pattern settlement leg now takes the counterparty template's learned legacy pair (credit for expense, debit for income, mirror-swapped, || 1930), passed raw from QuickReviewDialog: two skeptics refuted the 1930 default (engine books e.g. 2440 from SIE-learned patterns; preview/prefill showed 1930). Declined CodeRabbit's two suggestions on #1894 deliberately: the 3740 rounding line keeps the engine's business-side placement for BOTH diff signs (parity contract; the engine's negative-diff imbalance cannot reach the ledger, commit_journal_entry rejects it; engine-side sign fix is a separate issue) and the naiveOreRound baseline stays raised to 622 (engineRound is a documented parity exception, not drift). [2026-08-25] The production-only _backfill_remaining_20260817 invoice repair snapshot is privilege-contained, not deleted or relocated: PR #1655 identifies it as the safety snapshot for the 337-row 2026-08-17 remaining_amount repair, but the repository establishes neither its retention classification nor approval to destroy financial evidence, so the migration enables RLS, revokes PUBLIC/anon/authenticated access, and limits service_role to read-only for an authorized follow-up review while postgres retains owner control. Neither PR #1655 nor repository history establishes the original repair's behandlingshistorik/rattelse traceability or whether any repaired invoice was linked to a posted voucher; verifying the repair's who/when/what trail and its relation to booked entries is an explicit compliance follow-up, not inferred or altered by this access fix. Default privileges stay unchanged in this scoped fix because Supabase's platform transition and the application's many existing implicit grants require a separate compatibility audit. [2026-08-25] Risk ID RISK-2026-08-25-INVOICE-BACKFILL-SNAPSHOT treatment record (PR #1901): Risk Owner and follow-up owner Emil; classification restricted financial remediation evidence pending BFL review; treatment preserves all 337 rows and merges only the anonymous-access containment; BFL retention/rattelse review deadline 2026-09-25; residual risk after containment Low, explicitly including postgres-owner bypass until that review. Retention or deletion requires the separate reviewed follow-up, and this PR must not delete or alter snapshot rows. This entry and PR #1901 are the repository-native Risk Treatment Plan reference because the repository has no risk register. +[2026-08-25] Plugin distribution goes through the Claude plugin directory (public GitHub link, claude plugin validate, submit from claude.ai admin-settings or Console), not an organisation marketplace: org marketplaces accept private/internal repos only and require the Claude GitHub App, so a public monorepo can never pass that dialog (the 'Repository not accessible' error is misleading). Install-time guidance is a /accounted:setup slash command, the convention Anthropic's own plugins use (commands/*-setup.md); no SETUP.md mechanism exists in the plugin spec. diff --git a/claude-plugin/.claude-plugin/plugin.json b/claude-plugin/.claude-plugin/plugin.json index 64200e1a..1929a82d 100644 --- a/claude-plugin/.claude-plugin/plugin.json +++ b/claude-plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "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": "1.0.0", + "version": "1.1.0", "author": { "name": "Accounted (erp-mafia)" }, diff --git a/claude-plugin/CONNECTORS.md b/claude-plugin/CONNECTORS.md new file mode 100644 index 00000000..f7dad52d --- /dev/null +++ b/claude-plugin/CONNECTORS.md @@ -0,0 +1,18 @@ +# Connectors + +This plugin bundles exactly one connector: the Accounted MCP server, the same server that backs the Accounted connector in Claude.ai and the `accounted-mcp` stdio bridge. + +| Connector | Server | Auth | What it reaches | +|-----------|--------|------|-----------------| +| Accounted | `https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted` | OAuth 2.1 (PKCE). Read-only scopes by default; write scopes are ticked explicitly on the consent screen. | The user's own companies in Accounted: ledger, transactions, invoices, VAT, payroll, reconciliation, year-end. | + +## How the connection works + +- **Lazy authentication.** The server answers `initialize`, `tools/list` and the documentation tools (`accounted_search_tools`, `accounted_list_skills`, `accounted_load_skill`) without any credentials. The first company-scoped call returns an authentication challenge, which Claude Code surfaces as `/mcp` → authenticate. +- **Account creation inside the sign-in.** A user who has no Accounted account creates one on the sign-in screen the challenge opens (BankID or e-mail + 2FA). No visit to the website first. `/accounted:setup` walks the whole flow, including creating the company from the conversation. +- **Every write is staged.** Write tools create a pending operation with a preview; nothing is booked until the user approves, either in chat via `accounted_approve_pending_operation` or in the web app. +- **Data stays in the user's tenant.** The connector only ever sees companies the signed-in user is a member of, enforced server-side per call. + +## Self-hosted Accounted + +Point the plugin at your own instance: remove the bundled server and add yours with `claude mcp add --transport http accounted "https://your-host/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted"`. The same OAuth flow, skills and commands apply. diff --git a/claude-plugin/README.md b/claude-plugin/README.md index 2f61d8f6..f47644c5 100644 --- a/claude-plugin/README.md +++ b/claude-plugin/README.md @@ -12,12 +12,13 @@ The official plugin for [Accounted](https://app.accounted.se), the open-source S /plugin install accounted@accounted ``` -Then run `/mcp` and authenticate with Accounted (OAuth consent screen; read-only scopes by default, write scopes are ticked explicitly). No account yet? Create it on that same screen, with BankID or e-mail. Start with `/accounted:start`: for a brand-new account it walks you through setting up the company (company form, organisationsnummer, VAT, fiscal year) right here in the conversation, then hands you the bank and Skatteverket connect links. +Then run `/accounted:setup`. It connects the Accounted connector (`/mcp` → authenticate; no account yet? create it on that sign-in screen, with BankID or e-mail), and for a brand-new account it sets up the company right here in the conversation (company form, organisationsnummer, VAT, fiscal year), then hands you the bank and Skatteverket connect links. After that, `/accounted:start` orients you in the books. ## Skills | Command | What it does | |---|---| +| `/accounted:setup` | First run: connect (create the account if needed) and set up the company from the conversation | | `/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 | diff --git a/claude-plugin/commands/setup.md b/claude-plugin/commands/setup.md new file mode 100644 index 00000000..9031ed53 --- /dev/null +++ b/claude-plugin/commands/setup.md @@ -0,0 +1,33 @@ +--- +description: Connect Accounted (create the account if needed) and set up the company from this conversation. Run once after installing the plugin. +--- + +The user just installed the Accounted plugin, or asked to get set up. Take them from "not connected" to "books are running" without sending them to the website first. Everything below happens in this conversation except the steps that legally need a human with BankID in a browser: creating the account, approving a bank consent, authorising Skatteverket. + +## Step 1: connect + +Call `accounted_get_agent_briefing`. + +- If it succeeds, the user is connected and has a company: say so, summarise the company in one line (name, form, method, VAT period), and stop here. Point at `/accounted:start` for orientation. +- If it fails with an authentication error, the connector is not connected yet. Tell the user: run `/mcp`, pick **accounted**, and authenticate. The browser opens Accounted's sign-in. **No account yet? Create it right there** ("Skapa konto"): BankID is fastest (about a minute, no e-mail confirmation); e-mail + password also works and asks for a 2FA app before consent. On the consent screen, read-only scopes are pre-ticked; leave **Företag: skriv** ticked so the company can be created from here. Then continue with Step 2. +- If it fails with `NO_COMPANY_YET`, the account exists but has no company yet: continue with Step 2. + +## Step 2: set up the company + +Call `accounted_load_skill("onboarding")` and follow it. In short: ask for the facts (company form, organisationsnummer, F-skatt, fiscal year, VAT registration and moms period, accounting method), then call `accounted_create_company` **without** `confirm` to get a preview, read the preview back in plain Swedish, and only after an explicit "ja" call it again with `confirm: true`. + +Rules the tool enforces, so do not argue with them: a VAT-registered company needs both an organisationsnummer and a moms period; F-skatt must be stated, never assumed; an enskild firma always runs on the calendar year. + +## Step 3: connections + +Call `accounted_connect_bank` and `accounted_connect_skatteverket`. Each returns a status and a link. The user opens the link in a browser where they are logged in to Accounted, approves with BankID, and comes back. Neither is mandatory to start: bank statements can also be imported as files, and declarations can always be downloaded and filed manually. + +## Step 4: hand over + +Call `accounted_get_agent_briefing` again to confirm the company is live, then point at the flows: `/accounted:bookkeep` once transactions arrive, `/accounted:check` for a health check, `/accounted:start` any time for orientation. + +## Rules + +- Never create a company "to try things out" for a real organisation: bookkeeping duty starts the moment it exists. Use the sandbox in the web app for demos. +- Never guess company facts. Every value in the preview came from the user or from the organisationsnummer lookup. +- Every write in Accounted stages for the user's approval; nothing is booked on its own. diff --git a/extensions/general/mcp-server/__tests__/claude-plugin-refs.test.ts b/extensions/general/mcp-server/__tests__/claude-plugin-refs.test.ts index f847aa66..8ea5d42b 100644 --- a/extensions/general/mcp-server/__tests__/claude-plugin-refs.test.ts +++ b/extensions/general/mcp-server/__tests__/claude-plugin-refs.test.ts @@ -15,6 +15,7 @@ import { discoverAtoms } from '@/scripts/lib/atom-discovery' const repoRoot = join(__dirname, '..', '..', '..', '..') const skillsDir = join(repoRoot, 'claude-plugin', 'skills') +const commandsDir = join(repoRoot, 'claude-plugin', 'commands') function readPluginSkills(): { file: string; body: string }[] { return readdirSync(skillsDir).map((dir) => { @@ -23,14 +24,24 @@ function readPluginSkills(): { file: string; body: string }[] { }) } -const pluginSkills = readPluginSkills() +// Slash commands (commands/*.md) reference the same server surface as the +// skills and get the same guard; /accounted:setup is the install-time entry +// (issue #1814) that hands off to the onboarding skill. +function readPluginCommands(): { file: string; body: string }[] { + return readdirSync(commandsDir) + .filter((f) => f.endsWith('.md')) + .map((f) => ({ file: `commands/${f}`, body: readFileSync(join(commandsDir, f), 'utf8') })) +} + +const pluginSkills = [...readPluginSkills(), ...readPluginCommands()] const serverSource = readFileSync(join(__dirname, '..', 'server.ts'), 'utf8') describe('claude-plugin wrapper references', () => { - it('ships the seven v1 skills', () => { + it('ships the seven v1 skills and the setup command', () => { expect(pluginSkills.map((s) => s.file).sort()).toEqual([ 'bookkeep/SKILL.md', 'check/SKILL.md', + 'commands/setup.md', 'month-close/SKILL.md', 'payroll/SKILL.md', 'start/SKILL.md',