diff --git a/DECISIONS.md b/DECISIONS.md index b9cd6471..0ac8df32 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1427,6 +1427,12 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-09-01] multi_user skeptic fixes: Stripe cancel EXPIRES the multi_user stripe grant instead of deleting it (grace anchor; other grants still deleted per freeze-and-retain); app-side state checks go RPC-first via SECURITY DEFINER company_multi_user_state (capability_grants RLS hides team rows from non-team users, byrå clients would misread as frozen); byra-kind teams get a standing team-scoped multi_user grant via backfill + teams trigger (WL-10 assumption made real; partner billing is out-of-band); PGRST202 on resolution fails OPEN (pre-migration DB has no multi_user rows: gated fallback would freeze all non-owners); /api/v1 got the same dormancy gate as MCP. RLS-level enforcement and the mid-session API fallback write-back window stay v2 follow-ups (documented, same class as pre-existing stale-preference fallback). [2026-09-01] Declined CodeRabbit's UpgradeNote suggestion (PR #1758 follow-up) to append the self-host connector sentence to children instead of replacing them: every caller's children is hosted subscription copy ("... kräver ett abonnemang"), so appending would show subscription wording on a self-host, the exact thing the branch exists to avoid; the "CSV/SIE import stays free" text it cited is a code comment in BankSyncNowButton, not children. Replace-on-self-host stays; a dedicated selfHosted children prop can come when a caller actually needs per-panel reassurance there. [2026-09-01] getConnectorConfig() rebuilds baseUrl as origin + path (userinfo/query/fragment stripped, warn-logged without the raw value): /api/connector/status echoes baseUrl to the operator and the sync/proxy URLs get paths appended, so nothing secret-shaped pasted into GNUBOK_CONNECT_URL may survive; the stripped parts were never meaningful in a base URL. The status route is also Cache-Control: no-store (key prefix + wiring layout out of shared browser caches). +[2026-09-01] Settings -> API renamed "API & MCP" and its MCP URLs now carry tool_namespace=accounted: resolveMcpToolNamespace() falls back to the legacy gnubok_ prefix when the param is absent, so every connection made from the in-product surface got `gnubok_*` tool names while the docs, the accounted-api skill, and claude-plugin/.mcp.json all reference `accounted_*`. The Claude Desktop block also moves from npx gnubok-mcp/GNUBOK_API_KEY to npx -y accounted-mcp/ACCOUNTED_API_KEY and emits ACCOUNTED_URL so self-hosted and white-label instances get a config pointing at their own host instead of app.accounted.se. The gnubok_sk_ key prefix itself is unchanged (wire format, per CLAUDE.md). +[2026-09-01] Connect-with-Claude ships in two languages as two URLs (/docs/api/connect-claude, /docs/api/anslut-claude) rather than adding locale routing to the docs site: the docs site has no i18n layer, the audience for the Swedish page is end users rather than integrators, and two hand-synced pages is a smaller standing cost than a locale system nobody else needs. Both are exported by scripts/export-docs-to-website.mts and both are asserted by scripts/check-docs-freshness.mts, so drift between repos is caught by the docs loop. +[2026-09-01] Corrects the 2026-07-27 line further down "Claude Code plugin homepage points at the GitHub README": the connect-claude page WAS ported to gnubok-website and docs.gnubok.se/connect-claude has returned 200 since. The plugin homepage can point at the docs page whenever it is next touched; no separate change opened for it. +[2026-09-01] Claude Code plugin install (/plugin marketplace add erp-mafia/accounted) surfaced in-product and in the docs. It is the only path that configures the connection AND the seven workflow commands in one step, and the marketplace manifest is public (verified: raw.githubusercontent 200, repo visibility PUBLIC). Directory listing (an Anthropic review that gets the connector a native Connect button and in-product discovery) is a separate founder task and still open; the one-click install link for custom connectors is unrelated to it and is covered below. +[2026-09-01] Settings -> API & MCP leads with a one-click "Connect to Claude" install link (https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=&connectorUrl=, Anthropic's documented format) instead of four equal-weight copy-blocks. Earlier in the same session I asserted no such deeplink existed; that was wrong, it is documented at claude.com/docs/connectors/building/directory-vs-custom. The link only prefills the dialog (user still reviews and confirms) and requires the instance to be reachable from Anthropic's cloud, so the manual URL stays as a fallback for localhost and firewalled self-hosts. Everything that needs a config file or a terminal (Claude Code, plugin, Cursor, Claude Desktop, API keys) moved behind two disclosures: the default panel went from four code blocks to one button, 1057px to 719px. +[2026-09-01] The connect-MCP group now renders above the API-keys group on /settings/api, and each entry inside the disclosures shows its instruction as visible text rather than a HelpPopover. Connecting a client is why users open the tab (the tab intro says so); key management is the secondary job, and four stacked `?` popovers meant the panel read as opaque code blobs with no instructions on screen. [2026-09-01] EB callback cross-company guard: an account the connection row itself already carried keeps its own enabled state even when a sibling company claims the IBAN (own-prior outranks claim): flipping a standing feed off on renewal would break legacy pre-session-sharing overlaps, while the F1 exposure only involves accounts NEW to the row. Claim-lookup failure fails closed to enabled:false but still mirrors (unknown claim state is not a proven foreign claim; skipping the mirror on a transient error would strand normal single-company connects). [2026-09-01] Provider-migration kreditfakturor import as ordinary invoice rows with reversed amounts and status 'credited', always unlinked. No provider DTO carries a reference to the credited invoice, so exact-match linking has no input to match on; guessing would corrupt the AR ledger. The wizard now says so rather than burying them in `skipped`. [2026-09-01] OAuth callback replay is fixed with no-store plus history replacement only, with no "Anslutningen ar redan klar" page. Keeps the anti-oracle property that consumed-vs-unknown state stays unobservable to an unauthenticated caller. diff --git a/app/docs/api/anslut-claude.md/route.ts b/app/docs/api/anslut-claude.md/route.ts new file mode 100644 index 00000000..2cdc0007 --- /dev/null +++ b/app/docs/api/anslut-claude.md/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server' +import { ANSLUT_CLAUDE_MD } from '@/lib/docs/content/anslut-claude' +import { withPublicSecurityHeaders } from '@/lib/api/v1/security-headers' + +export async function GET() { + return new NextResponse(ANSLUT_CLAUDE_MD, { + status: 200, + headers: withPublicSecurityHeaders({ + 'Content-Type': 'text/markdown; charset=utf-8', + 'Cache-Control': 'public, max-age=300, s-maxage=300', + }), + }) +} diff --git a/app/docs/api/anslut-claude/page.tsx b/app/docs/api/anslut-claude/page.tsx new file mode 100644 index 00000000..907569a8 --- /dev/null +++ b/app/docs/api/anslut-claude/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next' +import { DocsLayout } from '@/components/docs/DocsLayout' +import { DocsMarkdown } from '@/lib/docs/markdown' +import { ANSLUT_CLAUDE_MD } from '@/lib/docs/content/anslut-claude' + +export const metadata: Metadata = { + title: 'Anslut Claude · accounted API', + description: + 'Koppla Accounted till Claude (claude.ai, Claude Desktop, Claude Code) via MCP-servern: OAuth 2.1-connector, Claude Code-plugin eller npx accounted-mcp.', +} + +export default function DocsApiAnslutClaudePage() { + return ( + + + + ) +} diff --git a/components/settings/ApiKeysPanel.tsx b/components/settings/ApiKeysPanel.tsx index 8e93f13d..9c619add 100644 --- a/components/settings/ApiKeysPanel.tsx +++ b/components/settings/ApiKeysPanel.tsx @@ -22,11 +22,9 @@ import { useToast } from '@/components/ui/use-toast' import { SettingsGroup, SettingsReveal, - SettingsRow, - SettingsRowNote, } from '@/components/settings/SettingsRows' import { AttnLine } from '@/components/ui/attn-line' -import { Loader2, Plus, Copy, Check, Trash2, Key, ChevronDown, AlertTriangle } from 'lucide-react' +import { Loader2, Plus, Copy, Check, Trash2, Key, ChevronDown, AlertTriangle, ArrowUpRight } from 'lucide-react' import { cn, formatDateLong } from '@/lib/utils' import { copyToClipboard } from '@/lib/browser/copy-to-clipboard' import { getBranding } from '@/lib/branding/service' @@ -175,6 +173,7 @@ export function ApiKeysPanel() { const [showCreateDialog, setShowCreateDialog] = useState(false) const [showKeyDialog, setShowKeyDialog] = useState(false) const [showApiKeyMethods, setShowApiKeyMethods] = useState(false) + const [showOtherClients, setShowOtherClients] = useState(false) const [newKeyName, setNewKeyName] = useState('') // 'live' by default: this is the general MCP-key surface and the dominant case // is a key for the user's real company. 'test' is an explicit opt-in: a @@ -299,15 +298,216 @@ export function ApiKeysPanel() { } } - const mcpBase = typeof window !== 'undefined' - ? `${window.location.origin}/api/extensions/ext/mcp-server/mcp` - : '/api/extensions/ext/mcp-server/mcp' - // Telemetry-only distribution-channel marker (server reads the `client` query - // param; never used for auth). Lets us measure which Claude surface connected. - const mcpUrl = (client: string) => `${mcpBase}?client=${client}` + // This panel is server-rendered before it hydrates, and window.location has + // no server equivalent. Reading the origin at render time therefore yields a + // relative URL in the first paint, and a click on the install link in that + // window would hand claude.ai a connectorUrl it cannot resolve. Resolve the + // origin after mount and withhold the link's href until it is known. + const [origin, setOrigin] = useState('') + useEffect(() => setOrigin(window.location.origin), []) + const mcpBase = `${origin}/api/extensions/ext/mcp-server/mcp` + // `tool_namespace=accounted` is load-bearing: resolveMcpToolNamespace() + // falls back to the legacy `gnubok_` tool prefix when the param is absent, + // so a URL without it hands the client tool names that none of our docs, + // skills, or the Claude Code plugin reference. `client` is a telemetry-only + // distribution marker (server reads it; never used for auth). + const mcpUrl = (client: string) => + `${mcpBase}?tool_namespace=accounted&client=${client}` + + // claude.ai install link: opens Add-custom-connector with name and URL + // prefilled. It only prefills the dialog, so the user still reviews and + // confirms, and Anthropic's cloud must be able to reach the URL: on + // localhost or a firewalled self-host the manual paste below is the path. + // https://claude.com/docs/connectors/building/directory-vs-custom + const claudeInstallUrl = + 'https://claude.ai/customize/connectors?modal=add-custom-connector' + + `&connectorName=${encodeURIComponent(branding.appName)}` + + `&connectorUrl=${encodeURIComponent(mcpUrl('claude-connector'))}` return ( <> + + {/* The marketing site's halftone AI marks (Claude, OpenAI): a quiet + "works with" cue, not chrome. Text carries the meaning; the marks + are decorative. */} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + + {/* eslint-disable-next-line @next/next/no-img-element */} + +
+

{t('works_with_ai')}

+
+ {/* claude.ai is the path for nearly everyone, and the install link + makes it one click, so it is the only thing above the fold. Every + other client needs a config file or a terminal, which is a + different job: it lives behind one disclosure instead of four + code blocks competing with the button. */} +
+ +

+ {t('connect_to_claude_help')} +

+
+ + + +
+
+

{t('claude_ai_manual')}

+

+ {t.rich('claude_ai_instructions', { + // Prose gets the brand's real casing; `connectorName` is the + // lowercased config key and reads wrong in a sentence. + connectorName: branding.appName, + path: (chunks) => {chunks}, + })} +

+ +
+ +
+

{t('claude_plugin_label')}

+

{t('claude_plugin_instructions')}

+ +
+ +
+

Claude Code

+

{t('terminal_runs_browser_login')}

+ {/* URL is quoted: unquoted `?` in the query string trips zsh globbing. */} + +
+ +
+

Cursor

+

+ {t.rich('cursor_instructions', { + code: (chunks) => {chunks}, + })} +

+ +
+
+
+ + + +
+
+

Claude Desktop

+

+ {t.rich('claude_desktop_instructions', { + code: (chunks) => {chunks}, + })} +

+ {/* ACCOUNTED_URL is emitted so self-hosted and white-label + instances get a config that points at their own host: the + bridge otherwise defaults to the hosted endpoint. The key + value keeps the `gnubok_sk_` wire prefix on purpose. */} + +
+ +
+

Claude Code

+

+ {t('terminal_with_api_key')} +

+ +
+
+
+ + {/* The step-by-step guide is canonical on the docs site, in one + language per URL (the docs site has no locale routing). Root-relative + so the /docs/api/* 308 in next.config.ts forwards to docs.gnubok.se. */} + + {t('full_guide_link')} + + +
+ {/* Group eyebrow with the group's primary action on the right. Styling mirrors SettingsGroup's label line; the "?" holds the old panel @@ -388,110 +588,6 @@ export function ApiKeysPanel() { )} - - {/* The marketing site's halftone AI marks (Claude, OpenAI): a quiet - "works with" cue, not chrome. Text carries the meaning; the marks - are decorative. */} -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - - {/* eslint-disable-next-line @next/next/no-img-element */} - -
-

{t('works_with_ai')}

-
- {chunks}, - })} - > - {t('recommended_badge')} -
- -
-
- - - {/* URL is quoted: unquoted `?` in the query string trips zsh globbing. */} -
- -
-
- - - -
-
-

Claude Desktop

-

- {t.rich('claude_desktop_instructions', { - code: (chunks) => {chunks}, - })} -

- -
- -
-

{t('claude_code_cursor')}

-

- {t('terminal_with_api_key')} -

- -
-
-
-
- {/* Create key dialog */} diff --git a/lib/docs/content/anslut-claude.ts b/lib/docs/content/anslut-claude.ts new file mode 100644 index 00000000..8d9a3a09 --- /dev/null +++ b/lib/docs/content/anslut-claude.ts @@ -0,0 +1,136 @@ +/** + * Swedish translation of CONNECT_CLAUDE_MD (lib/docs/content/connect-claude.ts). + * + * The docs site has no locale routing, so the two languages live at two URLs + * and cross-link to each other. Keep them in sync: an edit to one is only half + * an edit. + */ +export const ANSLUT_CLAUDE_MD = `# Anslut Claude + +> Prata med din bokföring. Koppla Accounted till Claude (claude.ai, Claude Desktop eller Claude Code) och ställ frågor, kontera transaktioner och förbered momsdeklarationen i vanligt språk. Varje skrivning stannar för ditt godkännande först. + +_This page in English: [Connect with Claude](/docs/api/connect-claude)._ + +Accounted har en [MCP](https://modelcontextprotocol.io)-server som exponerar hela bokföringsmotorn (150+ verktyg) för vilken MCP-klient som helst. Adressen är: + +\`\`\` +https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted +\`\`\` + +Det finns tre vägar in, beroende på vilken klient du använder. Alla tre når samma verktyg och samma godkännandemodell: läsverktyg svarar direkt, medan skrivverktyg (kontera, markera betald, skapa verifikat, bokslut) **lägger upp en pending operation** som du bekräftar i chatten eller i webbgränssnittet under **/pending** innan något bokförs. + +## Väg A: claude.ai eller Claude Desktop (ett klick) + +**[→ Anslut Accounted till Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Accounted&connectorUrl=https%3A%2F%2Fapp.accounted.se%2Fapi%2Fextensions%2Fext%2Fmcp-server%2Fmcp%3Ftool_namespace%3Daccounted%26client%3Dclaude-connector)** + +Länken öppnar claude.ai med namn och adress ifyllda. Du granskar värdena och godkänner; länken fyller bara i formuläret och ger ingenting i sig. Ingen API-nyckel att hålla reda på. + +**Du behöver inget Accounted-konto ännu.** Anslutningen fungerar direkt: servern svarar på handskakningen och dokumentationsverktygen utan inloggning, och första anropet som rör ett bolag öppnar Accounteds inloggning, där du som ny skapar kontot (BankID eller e-post + 2FA). + +**Läsrättigheter som standard.** På godkännandesidan väljer du bolag och ger läsrättigheter (lista fakturor, läsa rapporter, räkna moms). Skrivrättigheter (skapa faktura, kontera, bokföra verifikat, köra bokslut) listas separat och måste bockas i uttryckligen. Så kan en granskare ansluta läsande medan du själv har en anslutning med skrivrättigheter för det dagliga arbetet. + +#### Lägga till manuellt i stället + +I **claude.ai** (Inställningar → Connectors) eller **Claude Desktop** (Inställningar → Connectors → Add custom connector), välj **Add custom connector** och klistra in: + +\`\`\` +https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-connector +\`\`\` + +Behåll \`tool_namespace=accounted\`: den väljer verktygsnamnen som guiden utgår från. \`client=claude-connector\` är bara statistik. Frågar dialogen om autentisering, välj **"Required when the server asks"**, inte det automatiskt föreslagna "None". + +## Väg B: Claude Code (plugin) + +Bäst i terminalen. Pluginet installerar både anslutningen och sju färdiga arbetsflöden som följer den svenska bokföringsrytmen. + +\`\`\`text +/plugin marketplace add erp-mafia/accounted +/plugin install accounted@accounted +\`\`\` + +Kör sedan \`/mcp\` och logga in med Accounted (samma OAuth-ruta som i väg A). Börja med \`/accounted:start\`. + +| Kommando | Vad det gör | +|---|---| +| \`/accounted:start\` | Ansluter, orienterar och visar vad som behöver göras | +| \`/accounted:bookkeep\` | Betar av obokförda banktransaktioner och kvitton | +| \`/accounted:check\` | Läsande hälsokoll med prioriterad åtgärdslista | +| \`/accounted:month-close\` | Stänger månaden mot produktens checklista | +| \`/accounted:vat\` | Förbereder och stämmer av momsdeklarationen | +| \`/accounted:payroll\` | Månadens lönekörning och underlag för AGI | +| \`/accounted:year-end\` | Bokslut, spärrat mot readiness-kontrollen | + +Vill du bara ha anslutningen utan arbetsflödena kopplar \`claude mcp add\` in samma server i Claude Code: + +\`\`\`bash +claude mcp add --transport http accounted \\ + "https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-code" +\`\`\` + +**Cursor** har inget pluginformat och läser inte \`claude mcp add\`. Lägg i stället till servern i \`~/.cursor/mcp.json\` (globalt) eller \`.cursor/mcp.json\` (per projekt): + +\`\`\`json +{ + "mcpServers": { + "accounted": { + "url": "https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=cursor" + } + } +} +\`\`\` + +## Väg C: \`npx accounted-mcp\` med API-nyckel + +Bäst när du hellre använder en långlivad API-nyckel än OAuth-flödet, eller när du skriptar. + +1. Skapa en API-nyckel i Accounted under **Inställningar → API & MCP**. Använd en \`gnubok_sk_test_*\`-nyckel mot sandlådan medan du utvärderar och byt till \`gnubok_sk_live_*\` för skarp data. +2. Lägg till bryggan i din \`claude_desktop_config.json\`: + \`\`\`json + { + "mcpServers": { + "accounted": { + "command": "npx", + "args": ["-y", "accounted-mcp"], + "env": { + "ACCOUNTED_API_KEY": "gnubok_sk_test_...", + "ACCOUNTED_CLIENT": "claude-desktop" + } + } + } + } + \`\`\` + Kör du Accounted i egen drift pekar du om bryggan med \`ACCOUNTED_URL\`. +3. Starta om Claude Desktop. Bryggan skickar vidare stdio-JSON-RPC till den hostade MCP-servern över HTTPS, och nyckeln bär de rättigheter du gav den när den skapades. + +Nyckelns rättigheter styr exakt vilka verktyg som går att kalla: en nyckel utan skrivrättigheter kan läsa rapporter och reskontror men inte lägga upp en bokföring. + +Nyckelvärdet börjar fortfarande med \`gnubok_sk_\`. Det är ett stabilt kreditformat, inte namnet på integrationen. Befintliga \`gnubok-mcp\`-konfigurationer fortsätter att fungera oförändrade. + +## Testa med de här frågorna + +Alla tre går mot den deterministiska sandlådan (använd en \`gnubok_sk_test_*\`-nyckel eller välj sandlådebolaget på godkännandesidan). De går igenom hela läsvägen utan att bokföra något. + +1. **"Visa mina okonterade banktransaktioner och föreslå konteringar."** + Claude kallar \`accounted_list_uncategorized_transactions\` och sedan \`accounted_suggest_categories\` och går igenom förslagen med dig. Godkänner du ett förslag läggs en \`accounted_categorize_transaction\` upp som pending operation. Ingenting bokförs förrän du bekräftar. +2. **"Vilka fakturor är förfallna?"** + Claude kallar \`accounted_get_ar_ledger\` (kundreskontra) och listar utestående kundfakturor med åldersfördelning. +3. **"Räkna fram momsen för det här kvartalet och säg om jag kan stänga."** + Claude kallar \`accounted_get_vat_report\` för momsdeklarationens rutor och sedan \`accounted_vat_close_check\` som letar efter stopp (okonterade rader, ej godkända leverantörsfakturor, saknade kvitton på utgifter över 4 000 kr, vilket är verktygets tröskel för väsentliga belopp; BFL kräver underlag för varje affärshändelse oavsett belopp) och rapporterar \`ready_to_close\`. + +## Tiominuterstestet + +En snabb genomgång som visar att anslutningen fungerar innan du släpper in den på skarp data. Kör stegen i ordning. Varje steg säger vad du gör och vad du ska se. + +1. **Anslut.** Väg A med bara läsrättigheter, väg B, eller väg C med en \`gnubok_sk_test_*\`-nyckel. → Claude listar Accounteds verktyg (rubriker som *List Uncategorized Transactions* och *VAT Declaration (Momsdeklaration)*). +2. **Kontrollera bolaget.** Fråga *"Vilket bolag är jag ansluten till?"* → Claude namnger sandlådebolaget (till exempel **Sandlådan Konsult**). +3. **Kör fråga 1** (okonterade och konteringsförslag). → En lista med okonterade rader plus förslag. Ingen bokföring sker. +4. **Kör fråga 2** (förfallna fakturor). → Minst en förfallen kundfaktura med åldersfördelning. +5. **Kör fråga 3** (moms och kan jag stänga). → Momsdeklarationens rutor plus en **icke-tom lista med stopp** från \`accounted_vat_close_check\` (okonterade transaktioner, en ej godkänd leverantörsfaktura och en större utgift utan kvitto). +6. **Testa en skrivning.** Be Claude kontera en transaktion. → Claude lägger upp en pending operation och ber dig bekräfta. Bokföringen sker **inte** förrän du godkänner i chatten eller på **/pending**. + +Stämmer varje steg är anslutningen rätt kopplad och godkännandemodellen på plats. + +## Support + +Fastnar du, eller ser ett stopp du inte förstår? Använd supportformuläret i appen under **/help**. Det går direkt till produktteamet med ditt bolag som kontext. Skriv med vilken klient du använder (claude.ai, Desktop eller Code), vilken väg du tog (A, B eller C) och verktygsnamnet från eventuellt felmeddelande. +` diff --git a/lib/docs/content/connect-claude.ts b/lib/docs/content/connect-claude.ts index 2e94abd1..1b352265 100644 --- a/lib/docs/content/connect-claude.ts +++ b/lib/docs/content/connect-claude.ts @@ -2,36 +2,81 @@ export const CONNECT_CLAUDE_MD = `# Connect with Claude > Talk to your bookkeeping. Connect Accounted to Claude (claude.ai, Claude Desktop, or Claude Code) and ask questions, categorise transactions, and prepare a momsdeklaration in plain language: every write still stages for your approval first. +_Den här sidan på svenska: [Anslut Claude](/docs/api/anslut-claude)._ + Accounted ships an [MCP](https://modelcontextprotocol.io) server that exposes the full bookkeeping engine (150+ tools) to any MCP client. The endpoint is: \`\`\` https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted \`\`\` -There are two ways to connect, depending on your client. Both reach the same tools and the same approval model: read tools answer immediately, write tools (categorise, mark paid, create voucher, year-end) **stage a pending operation** that you confirm in chat or in the **/pending** web UI before anything is booked. +There are three ways to connect, depending on your client. All three reach the same tools and the same approval model: read tools answer immediately, write tools (categorise, mark paid, create voucher, year-end) **stage a pending operation** that you confirm in chat or in the **/pending** web UI before anything is booked. -## Path A: claude.ai / Claude Desktop custom connector (OAuth 2.1) +## Path A: claude.ai or Claude Desktop (one click) -Best for most users. No API key to manage: you authorise Accounted the same way you'd authorise any OAuth app. +**[→ Connect Accounted to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Accounted&connectorUrl=https%3A%2F%2Fapp.accounted.se%2Fapi%2Fextensions%2Fext%2Fmcp-server%2Fmcp%3Ftool_namespace%3Daccounted%26client%3Dclaude-connector)** -1. In **claude.ai** (Settings → Connectors) or **Claude Desktop** (Settings → Connectors → Add custom connector), choose **Add custom connector**. -2. Paste the connector URL: - \`\`\` - https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-connector - \`\`\` - _The "client=claude-connector" parameter is telemetry-only. Keep "tool_namespace=accounted": it selects the Accounted tool names._ - If the dialog asks about authentication, choose **"Required when the server asks"** (not the auto-detected "None") and let Claude register an OAuth client automatically. -3. You do not need an Accounted account yet. The connector works as soon as it is added: the server answers the handshake and the documentation tools (search tools, list and load skills) without credentials, and the first company-scoped call opens the Accounted sign-in. A new user creates the account right there (BankID or e-mail + 2FA), no visit to the website first. -4. Sign in and pick the company you want Claude to act on. On the consent screen you grant **read-only scopes by default** (list invoices, read reports, compute VAT). Write scopes (create invoice, categorise, book vouchers, run year-end) are **listed separately and must be ticked explicitly**: leave them unchecked for a read-only review session. -5. Approve. Claude now lists the Accounted tools and you can start asking questions. +The link opens claude.ai with the connector name and URL already filled in. You review the values and confirm; the link only prefills the dialog, it grants nothing on its own. No API key to manage. -Because the consent is per-company and scoped, you can connect a read-only key for a reviewer and a separate write-enabled connection for day-to-day bookkeeping. +**You do not need an Accounted account yet.** The connector works as soon as it is added: the server answers the handshake and the documentation tools without credentials, and the first company-scoped call opens the Accounted sign-in, where a new user creates the account (BankID or e-mail + 2FA). -## Path B: \`npx accounted-mcp\` with an API key (stdio bridge) +**Read-only by default.** On the consent screen you pick the company and grant read scopes (list invoices, read reports, compute VAT). Write scopes (create invoice, categorise, book vouchers, run year-end) are listed separately and must be ticked explicitly, so a reviewer can connect read-only while you keep a write-enabled connection for daily work. + +#### Adding it by hand instead + +In **claude.ai** (Settings → Connectors) or **Claude Desktop** (Settings → Connectors → Add custom connector), choose **Add custom connector** and paste: + +\`\`\` +https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-connector +\`\`\` + +Keep \`tool_namespace=accounted\`: it selects the tool names this guide uses. \`client=claude-connector\` is telemetry-only. If the dialog asks about authentication, choose **"Required when the server asks"**, not the auto-detected "None". + +## Path B: Claude Code (plugin) + +Best in the terminal. The plugin installs the connection *and* seven workflow commands that follow the Swedish bookkeeping rhythm. + +\`\`\`text +/plugin marketplace add erp-mafia/accounted +/plugin install accounted@accounted +\`\`\` + +Then run \`/mcp\` and sign in with Accounted (the same OAuth consent screen as Path A). Start with \`/accounted:start\`. + +| Command | What it does | +|---|---| +| \`/accounted:start\` | Connect, orient, and surface what needs attention | +| \`/accounted:bookkeep\` | Clear unbooked bank transactions and receipts | +| \`/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 | + +Prefer plain MCP without the workflow commands? \`claude mcp add\` wires the same connection into Claude Code: + +\`\`\`bash +claude mcp add --transport http accounted \\ + "https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=claude-code" +\`\`\` + +**Cursor** has no plugin format and does not read \`claude mcp add\`. Add the server to \`~/.cursor/mcp.json\` (global) or \`.cursor/mcp.json\` (per project) instead: + +\`\`\`json +{ + "mcpServers": { + "accounted": { + "url": "https://app.accounted.se/api/extensions/ext/mcp-server/mcp?tool_namespace=accounted&client=cursor" + } + } +} +\`\`\` + +## Path C: \`npx accounted-mcp\` with an API key (stdio bridge) Best for Claude Desktop on a machine where you'd rather use a long-lived API key than the OAuth flow, or for scripting. -1. Mint an API key in the Accounted dashboard at **/settings/api**. Use a \`gnubok_sk_test_*\` key against the sandbox while you evaluate; switch to \`gnubok_sk_live_*\` for real data. +1. Mint an API key in the Accounted dashboard under **Settings → API & MCP** (\`/settings/api\`). Use a \`gnubok_sk_test_*\` key against the sandbox while you evaluate; switch to \`gnubok_sk_live_*\` for real data. 2. Add the stdio bridge to your \`claude_desktop_config.json\`: \`\`\`json { @@ -47,6 +92,7 @@ Best for Claude Desktop on a machine where you'd rather use a long-lived API key } } \`\`\` + Running Accounted yourself? Point the bridge at your own host with \`ACCOUNTED_URL\`. 3. Restart Claude Desktop. The bridge proxies stdio JSON-RPC to the hosted MCP endpoint over HTTPS; the key carries the scopes you granted it at mint time. The key's scopes gate exactly which tools are callable: a key without write scopes can read reports and ledgers but cannot stage a booking. @@ -70,16 +116,16 @@ All three run against the deterministic sandbox seed (use a \`gnubok_sk_test_*\` A quick end-to-end pass to confirm the connection works before you trust it with real data. Run the steps in order; each lists what you do and what you should see. -1. **Connect.** Use Path A (read-only scopes only) or Path B with a \`gnubok_sk_test_*\` key. → Claude lists the Accounted tools (titles like *List Uncategorized Transactions*, *VAT Declaration (Momsdeklaration)*). +1. **Connect.** Use Path A (read-only scopes only), Path B, or Path C with a \`gnubok_sk_test_*\` key. → Claude lists the Accounted tools (titles like *List Uncategorized Transactions*, *VAT Declaration (Momsdeklaration)*). 2. **Confirm the company.** Ask *"Which company am I connected to?"* → Claude names the sandbox company (e.g. **Sandlådan Konsult**). 3. **Run prompt 1** (*uncategorized + suggest categories*). → A list of uncategorised rows plus category suggestions; no booking happens. 4. **Run prompt 2** (*overdue invoices*). → At least one overdue customer invoice with aging. -5. **Run prompt 3** (*VAT report + can I close*). → Momsdeklaration rutor returned; \`vat_close_check\` reports a **non-empty blocker list** (uncategorised transactions, an unapproved leverantörsfaktura, and a high-value business expense without a receipt). +5. **Run prompt 3** (*VAT report + can I close*). → Momsdeklaration rutor returned; \`accounted_vat_close_check\` reports a **non-empty blocker list** (uncategorised transactions, an unapproved leverantörsfaktura, and a high-value business expense without a receipt). 6. **Stage a write.** Ask Claude to categorise one transaction. → Claude stages a pending operation and asks you to confirm: the booking does **not** post until you approve in chat or at **/pending**. If every step matches, the connector is wired correctly and the approval model is enforced. ## Support -Stuck connecting, or seeing an unexpected blocker? Use the in-app support form at **/help**: it routes straight to the product team with your company context attached. Include the client (claude.ai / Desktop / Code), the path you used (A or B), and the tool name from any error message. +Stuck connecting, or seeing an unexpected blocker? Use the in-app support form at **/help**: it routes straight to the product team with your company context attached. Include the client (claude.ai / Desktop / Code), the path you used (A, B, or C), and the tool name from any error message. ` diff --git a/lib/docs/nav.ts b/lib/docs/nav.ts index ce09ac20..a0498417 100644 --- a/lib/docs/nav.ts +++ b/lib/docs/nav.ts @@ -27,7 +27,8 @@ export const DOCS_NAV: DocsNavSection[] = [ { label: 'Introduction', href: '/docs/api', summary: 'What the Accounted REST API is and how to authenticate.' }, { label: 'Quickstart', href: '/docs/api/cookbook/quickstart', summary: 'Send your first invoice in five minutes.' }, { label: 'Authentication', href: '/docs/api#authentication', summary: 'API keys, scopes, test mode.' }, - { label: 'Connect with Claude', href: '/docs/api/connect-claude', summary: 'Connect Claude via the MCP server: OAuth connector or npx accounted-mcp bridge.' }, + { label: 'Connect with Claude', href: '/docs/api/connect-claude', summary: 'Connect Claude via the MCP server: OAuth connector, Claude Code plugin, or npx accounted-mcp bridge.' }, + { label: 'Anslut Claude (svenska)', href: '/docs/api/anslut-claude', summary: 'Samma guide på svenska: OAuth-connector, Claude Code-plugin eller npx accounted-mcp.' }, ], }, { diff --git a/messages/en.json b/messages/en.json index 3693d058..b758b19f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -392,7 +392,7 @@ "templates": "Reusable postings and patterns learned from your bookkeeping.", "banking": "Transactions are fetched automatically. {appName} can only read, never move money.", "assistant": "What the assistant knows, remembers and can do.", - "api": "Keys for MCP clients like Claude and Cursor, and for your own integrations.", + "api": "Connect Claude, Cursor, and other AI assistants over MCP. API keys for your own integrations live here too.", "whatsapp": "Link your mobile number and send receipts as photos or PDFs straight from WhatsApp." }, "settings_nav": { @@ -414,7 +414,7 @@ "assistant": "Assistant", "backup": "Backup", "account": "Account", - "api": "API", + "api": "API & MCP", "billing": "Subscription", "whatsapp": "WhatsApp", "group_account": "Account", @@ -2838,13 +2838,19 @@ "copy_aria": "Copy", "copy_failed": "Could not copy. Select the text above and copy it manually.", "connect_mcp_title": "Connect MCP client", + "connect_to_claude": "Connect to Claude", + "connect_to_claude_help": "Opens claude.ai with the URL prefilled. You review and confirm there; no API key needed. Your instance must be reachable from the internet.", + "other_clients": "Other clients: Claude Code, Cursor, plugin", + "claude_ai_manual": "claude.ai (manual)", "works_with_ai": "Works with Claude, ChatGPT, and other AI assistants.", - "recommended_badge": "Recommended", "claude_ai_instructions": "Go to Settings → Connectors → Add custom connector and paste the MCP server URL. You sign in via your {connectorName} account: no API key needed.", - "claude_code_cursor": "Claude Code / Cursor", + "cursor_instructions": "Add to ~/.cursor/mcp.json (global) or .cursor/mcp.json (per project). Cursor does not read claude mcp add.", "terminal_runs_browser_login": "Run in the terminal, sign in via the browser:", - "connect_with_api_key": "Connect with an API key instead", + "connect_with_api_key": "Connect with an API key", "claude_desktop_instructions": "Add to claude_desktop_config.json (Settings → Developer):", + "claude_plugin_label": "Claude Code plugin", + "claude_plugin_instructions": "Installs both the connection and seven ready-made workflows (/accounted:start, /accounted:vat, /accounted:year-end). Run these lines in Claude Code, then approve the sign-in with /mcp.", + "full_guide_link": "Full guide: connect Claude step by step", "terminal_with_api_key": "Run in the terminal with an API key:", "create_dialog_title": "Create API key", "create_dialog_description": "Name the key so you remember what it's for.", diff --git a/messages/sv.json b/messages/sv.json index 102c1cab..0e0abf25 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -392,7 +392,7 @@ "templates": "Återanvändbara konteringar och mönster som lärts in från din bokföring.", "banking": "Transaktioner hämtas automatiskt. {appName} kan bara läsa, aldrig flytta pengar.", "assistant": "Vad assistenten vet, minns och kan.", - "api": "Nycklar för MCP-klienter som Claude och Cursor, och för egna integrationer.", + "api": "Anslut Claude, Cursor och andra AI-assistenter via MCP. Här skapar du också API-nycklar för egna integrationer.", "whatsapp": "Koppla ditt mobilnummer och skicka kvitton som foto eller PDF direkt i WhatsApp." }, "settings_nav": { @@ -414,7 +414,7 @@ "assistant": "Assistenten", "backup": "Säkerhetsbackup", "account": "Konto", - "api": "API", + "api": "API & MCP", "billing": "Abonnemang", "whatsapp": "WhatsApp", "group_account": "Konto", @@ -2838,13 +2838,19 @@ "copy_aria": "Kopiera", "copy_failed": "Kunde inte kopiera. Markera texten ovan och kopiera manuellt.", "connect_mcp_title": "Anslut MCP-klient", + "connect_to_claude": "Anslut till Claude", + "connect_to_claude_help": "Öppnar claude.ai med adressen ifylld. Du granskar och godkänner där; ingen API-nyckel behövs. Kräver att din instans går att nå från internet.", + "other_clients": "Andra klienter: Claude Code, Cursor, plugin", + "claude_ai_manual": "claude.ai (manuellt)", "works_with_ai": "Fungerar med Claude, ChatGPT och andra AI-assistenter.", - "recommended_badge": "Rekommenderat", "claude_ai_instructions": "Gå till Settings → Connectors → Add custom connector och klistra in MCP-serverns URL. Du loggas in via ditt {connectorName}-konto: ingen API-nyckel behövs.", - "claude_code_cursor": "Claude Code / Cursor", + "cursor_instructions": "Lägg till i ~/.cursor/mcp.json (globalt) eller .cursor/mcp.json (per projekt). Cursor läser inte claude mcp add.", "terminal_runs_browser_login": "Kör i terminalen, loggar in via webbläsaren:", - "connect_with_api_key": "Anslut med API-nyckel istället", + "connect_with_api_key": "Anslut med API-nyckel", "claude_desktop_instructions": "Lägg till i claude_desktop_config.json (Inställningar → Developer):", + "claude_plugin_label": "Claude Code-plugin", + "claude_plugin_instructions": "Installerar både anslutningen och sju färdiga arbetsflöden (/accounted:start, /accounted:vat, /accounted:year-end). Kör raderna i Claude Code och godkänn sedan inloggningen med /mcp.", + "full_guide_link": "Full guide: anslut Claude steg för steg", "terminal_with_api_key": "Kör i terminalen med en API-nyckel:", "create_dialog_title": "Skapa API-nyckel", "create_dialog_description": "Ge nyckeln ett namn så du vet vad den används till.", diff --git a/scripts/check-docs-freshness.mts b/scripts/check-docs-freshness.mts index 3add955a..f5e9d299 100644 --- a/scripts/check-docs-freshness.mts +++ b/scripts/check-docs-freshness.mts @@ -40,6 +40,7 @@ ModuleCtor._load = function (request: string, ...rest: unknown[]) { let errors!: typeof import('@/lib/docs/content/errors') let reference!: typeof import('@/lib/docs/content/reference') let connectClaude!: typeof import('@/lib/docs/content/connect-claude') +let anslutClaude!: typeof import('@/lib/docs/content/anslut-claude') let changelog!: typeof import('@/lib/docs/content/changelog') let versioning!: typeof import('@/lib/docs/content/versioning') let webhooks!: typeof import('@/lib/docs/content/webhooks') @@ -48,6 +49,7 @@ try { errors = await import('@/lib/docs/content/errors') reference = await import('@/lib/docs/content/reference') connectClaude = await import('@/lib/docs/content/connect-claude') + anslutClaude = await import('@/lib/docs/content/anslut-claude') changelog = await import('@/lib/docs/content/changelog') versioning = await import('@/lib/docs/content/versioning') webhooks = await import('@/lib/docs/content/webhooks') @@ -100,6 +102,7 @@ function buildExpectedPages(): PageCheck[] { { path: 'reference.md', expected: buildReferenceOverviewMd() }, { path: 'errors.md', expected: buildErrorReferenceMd() }, { path: 'connect-claude.md', expected: connectClaude.CONNECT_CLAUDE_MD }, + { path: 'anslut-claude.md', expected: anslutClaude.ANSLUT_CLAUDE_MD }, { path: 'changelog.md', expected: changelog.CHANGELOG_MD }, { path: 'versioning.md', expected: versioning.VERSIONING_MD }, { path: 'webhooks.md', expected: webhooks.WEBHOOKS_MD }, diff --git a/scripts/export-docs-to-website.mts b/scripts/export-docs-to-website.mts index d85b81e2..ce5a1015 100644 --- a/scripts/export-docs-to-website.mts +++ b/scripts/export-docs-to-website.mts @@ -1,7 +1,8 @@ /** * One-shot script that exports the registry-derived docs content (errors + * reference) plus the static Connect-with-Claude page as TypeScript modules - * into the gnubok-website repo. + * into the gnubok-website repo. Connect-with-Claude ships in both languages: + * the docs site has no locale routing, so each language is its own page. * * Run with `npx tsx scripts/export-docs-to-website.mts`. Re-run whenever * structured-errors, the v1 endpoint registry, or connect-claude materially @@ -27,11 +28,12 @@ ModuleCtor._load = function (request: string, ...rest: unknown[]) { return originalLoad.call(this, request, ...rest) } -let errors, reference, connectClaude +let errors, reference, connectClaude, anslutClaude try { errors = await import('@/lib/docs/content/errors') reference = await import('@/lib/docs/content/reference') connectClaude = await import('@/lib/docs/content/connect-claude') + anslutClaude = await import('@/lib/docs/content/anslut-claude') } finally { // Scope the stub to the imports that need it: leaving a global loader hook // patched for the rest of the process would silently disarm the guard for @@ -106,4 +108,14 @@ write( `// AUTO-GENERATED from erp-base: do not hand-edit.\n// Regenerate via \`npx tsx scripts/export-docs-to-website.mts\` in erp-base.\nexport const CONNECT_CLAUDE_MD = ${JSON.stringify(connectClaudeMd)}\n`, ) +const anslutClaudeMd = anslutClaude.ANSLUT_CLAUDE_MD && adaptForWebsite(anslutClaude.ANSLUT_CLAUDE_MD) +if (!anslutClaudeMd) { + console.error('Missing ANSLUT_CLAUDE_MD export. Inspect:', { anslutClaudeKeys: Object.keys(anslutClaude) }) + process.exit(1) +} +write( + 'lib/docs/content/anslut-claude.generated.ts', + `// AUTO-GENERATED from erp-base: do not hand-edit.\n// Regenerate via \`npx tsx scripts/export-docs-to-website.mts\` in erp-base.\nexport const ANSLUT_CLAUDE_MD = ${JSON.stringify(anslutClaudeMd)}\n`, +) + console.log('done.')