feat(settings): API & MCP tab: correct connector URL namespace, plugin path, Swedish guide (#2105)
* feat(settings): API & MCP tab: correct connector URL namespace, plugin path, Swedish guide The in-product MCP URLs omitted `tool_namespace=accounted`, and resolveMcpToolNamespace() falls back to the legacy `gnubok_` prefix when the param is absent. Every connection made from Settings therefore got `gnubok_*` tool names while the docs, the accounted-api skill, and claude-plugin/.mcp.json all reference `accounted_*`. - Add `tool_namespace=accounted` to the Claude.ai, Claude Code, and Claude Desktop snippets. - Move the Claude Desktop bridge from `npx gnubok-mcp` / `GNUBOK_API_KEY` to `npx -y accounted-mcp` / `ACCOUNTED_API_KEY`, and emit `ACCOUNTED_URL` so self-hosted and white-label instances get a config pointing at their own host. The `gnubok_sk_` key prefix is unchanged: it is wire format. - Surface the Claude Code plugin, the only path that configures the connection and the seven workflow commands in one step. - Rename the settings tab "API" to "API & MCP" and rewrite its intro: the MCP connection is what most users come here for, not API keys. - Link the step-by-step guide from the panel, locale-aware. Docs: add a Swedish /docs/api/anslut-claude alongside the English page (the docs site has no locale routing, so each language is its own URL), give both the Claude Code plugin path, and teach the export and freshness scripts about the new page so cross-repo drift is caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfZgiZmNN6qGEfgvXqxAeg * fix(settings,docs): Cursor is not Claude Code; use the accounted_ tool name Review follow-up on #2105. `claude mcp add` is a Claude Code command. Cursor does not read it, so the "Claude Code / Cursor" row and the docs sentence pointing Cursor users at that command were both wrong (the row predates this PR; the docs sentence did not). Cursor now gets its own row and its own `~/.cursor/mcp.json` snippet with the `url` field, in the panel and in both docs pages. Also `vat_close_check` -> `accounted_vat_close_check` in the reviewer test on both pages, matching the identifier used in the prompts section above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfZgiZmNN6qGEfgvXqxAeg * feat(settings,docs): one-click Connect to Claude, and cut the panel to one action Anthropic documents an install link for custom connectors: https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=NAME&connectorUrl=ENCODED (claude.com/docs/connectors/building/directory-vs-custom). It opens claude.ai with the connector name and URL prefilled; the user still reviews and confirms, and it grants nothing on its own. We were telling people to copy a URL and go paste it somewhere else instead. Settings panel, rendered and reviewed: - "Connect to Claude" button is now the only thing above the fold. Everything that needs a config file or a terminal (claude.ai manual paste, Claude Code, the plugin, Cursor) moved into one "Other clients" disclosure, and the API-key methods keep theirs. Four code blocks -> one button, 1057px -> 719px. - The connect group renders above the API-keys group. Connecting is why users open this tab; the tab's own intro says so. - Each entry inside the disclosures shows its instruction as visible text. They were `?` HelpPopovers, so the panel read as opaque code blobs with no instructions on screen. - Prose interpolates the brand's real casing, not the lowercased config key. Docs, both languages: Path A leads with the install link and drops from five manual steps to a link plus three short paragraphs, with the manual paste kept under a subheading. No raw HTML: the docs renderer has no rehype-raw, so <details> would have been silently dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfZgiZmNN6qGEfgvXqxAeg * fix(settings,docs): correct claude mcp add syntax and the SSR install link Review follow-up. Both findings verified before acting on them. `claude mcp add --help` gives `claude mcp add [options] <name> <commandOrUrl>`: the URL is positional and there is no `--url` flag, so the API-key snippet would have failed on a missing argument. Both commands now put `--transport http` before the name and pass the URL positionally, in the panel and in both docs pages. The panel is server-rendered before it hydrates and window.location has no server equivalent, so the install link was built from a relative mcpBase in the first paint. A click in that window would hand claude.ai a connectorUrl it cannot resolve. The origin now resolves after mount and the anchor carries no href until it is known, which also makes it unclickable rather than wrong. Verified: the SSR HTML contains no claude.ai href and no relative connectorUrl, post-hydration the href is absolute, and there are no hydration warnings. DECISIONS.md: code-span the `gnubok_*`/`accounted_*` wildcards so they stop rendering as emphasis, and drop the "no one-click deeplink" claim from the earlier entry rather than leave a false statement standing two lines above its own correction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HfZgiZmNN6qGEfgvXqxAeg --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
Jakob Wennberg
parent
cd40127f0e
commit
b5da51ea0a
@@ -1427,6 +1427,12 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. 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.
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -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 (
|
||||
<DocsLayout currentPath="/docs/api/anslut-claude">
|
||||
<DocsMarkdown source={ANSLUT_CLAUDE_MD} />
|
||||
</DocsLayout>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<SettingsGroup label={t('connect_mcp_title')}>
|
||||
{/* The marketing site's halftone AI marks (Claude, OpenAI): a quiet
|
||||
"works with" cue, not chrome. Text carries the meaning; the marks
|
||||
are decorative. */}
|
||||
<div className="flex items-center gap-3 px-1 pb-1 pt-3">
|
||||
<div aria-hidden className="flex shrink-0 items-center gap-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={illustrationSrc('logo-claude')}
|
||||
width={ILLUSTRATIONS['logo-claude'].w}
|
||||
height={ILLUSTRATIONS['logo-claude'].h}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-5 w-auto"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={illustrationSrc('logo-openai')}
|
||||
width={ILLUSTRATIONS['logo-openai'].w}
|
||||
height={ILLUSTRATIONS['logo-openai'].h}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-5 w-auto"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('works_with_ai')}</p>
|
||||
</div>
|
||||
{/* 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. */}
|
||||
<div className="px-1 pb-4 pt-2">
|
||||
<Button asChild size="lg">
|
||||
<a
|
||||
href={origin ? claudeInstallUrl : undefined}
|
||||
aria-disabled={!origin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('connect_to_claude')}
|
||||
<ArrowUpRight className="ml-1.5 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<p className="mt-2 max-w-prose text-xs text-muted-foreground">
|
||||
{t('connect_to_claude_help')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={showOtherClients}
|
||||
onClick={() => setShowOtherClients(!showOtherClients)}
|
||||
className="flex w-full items-center gap-2 border-t border-border px-1 py-3 text-xs text-muted-foreground transition-colors duration-150 hover:text-foreground"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 transition-transform duration-150',
|
||||
!showOtherClients && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
{t('other_clients')}
|
||||
</button>
|
||||
<SettingsReveal open={showOtherClients}>
|
||||
<div className="space-y-6 pb-3 pt-1">
|
||||
<div>
|
||||
<p className="mb-1 text-sm">{t('claude_ai_manual')}</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{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) => <strong>{chunks}</strong>,
|
||||
})}
|
||||
</p>
|
||||
<CopyBlock text={mcpUrl('claude-connector')} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">{t('claude_plugin_label')}</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">{t('claude_plugin_instructions')}</p>
|
||||
<CopyBlock
|
||||
text={`/plugin marketplace add erp-mafia/accounted\n/plugin install accounted@accounted`}
|
||||
copyAriaLabel={t('copy_aria')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">Claude Code</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">{t('terminal_runs_browser_login')}</p>
|
||||
{/* URL is quoted: unquoted `?` in the query string trips zsh globbing. */}
|
||||
<CopyBlock text={`claude mcp add --transport http ${connectorName} "${mcpUrl('claude-code')}"`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">Cursor</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t.rich('cursor_instructions', {
|
||||
code: (chunks) => <code className="text-xs">{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
<CopyBlock text={`{
|
||||
"mcpServers": {
|
||||
"${connectorName}": {
|
||||
"url": "${mcpUrl('cursor')}"
|
||||
}
|
||||
}
|
||||
}`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingsReveal>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={showApiKeyMethods}
|
||||
onClick={() => setShowApiKeyMethods(!showApiKeyMethods)}
|
||||
className="flex w-full items-center gap-2 border-t border-border px-1 py-3 text-xs text-muted-foreground transition-colors duration-150 hover:text-foreground"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 transition-transform duration-150',
|
||||
!showApiKeyMethods && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
{t('connect_with_api_key')}
|
||||
</button>
|
||||
<SettingsReveal open={showApiKeyMethods}>
|
||||
<div className="space-y-6 pb-3 pt-1">
|
||||
<div>
|
||||
<p className="mb-1 text-sm">Claude Desktop</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t.rich('claude_desktop_instructions', {
|
||||
code: (chunks) => <code className="text-xs">{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
{/* 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. */}
|
||||
<CopyBlock text={`{
|
||||
"mcpServers": {
|
||||
"${connectorName}": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "accounted-mcp"],
|
||||
"env": {
|
||||
"ACCOUNTED_API_KEY": "gnubok_sk_...",
|
||||
"ACCOUNTED_URL": "${mcpUrl('claude-desktop')}",
|
||||
"ACCOUNTED_CLIENT": "claude-desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">Claude Code</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t('terminal_with_api_key')}
|
||||
</p>
|
||||
<CopyBlock text={`claude mcp add --transport http ${connectorName} \\
|
||||
"${mcpUrl('claude-code')}" \\
|
||||
--header "Authorization: Bearer gnubok_sk_..."`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingsReveal>
|
||||
|
||||
{/* 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. */}
|
||||
<a
|
||||
href={locale === 'sv' ? '/docs/api/anslut-claude' : '/docs/api/connect-claude'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-1 py-3 text-xs text-muted-foreground underline-offset-4 transition-colors duration-150 hover:text-foreground hover:underline"
|
||||
>
|
||||
{t('full_guide_link')}
|
||||
<ArrowUpRight className="h-3 w-3" />
|
||||
</a>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup>
|
||||
{/* 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() {
|
||||
)}
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup label={t('connect_mcp_title')}>
|
||||
{/* The marketing site's halftone AI marks (Claude, OpenAI): a quiet
|
||||
"works with" cue, not chrome. Text carries the meaning; the marks
|
||||
are decorative. */}
|
||||
<div className="flex items-center gap-3 px-1 pb-1 pt-3">
|
||||
<div aria-hidden className="flex shrink-0 items-center gap-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={illustrationSrc('logo-claude')}
|
||||
width={ILLUSTRATIONS['logo-claude'].w}
|
||||
height={ILLUSTRATIONS['logo-claude'].h}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-5 w-auto"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={illustrationSrc('logo-openai')}
|
||||
width={ILLUSTRATIONS['logo-openai'].w}
|
||||
height={ILLUSTRATIONS['logo-openai'].h}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-5 w-auto"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('works_with_ai')}</p>
|
||||
</div>
|
||||
<SettingsRow
|
||||
label="Claude.ai"
|
||||
align="baseline"
|
||||
help={t.rich('claude_ai_instructions', {
|
||||
connectorName,
|
||||
path: (chunks) => <strong>{chunks}</strong>,
|
||||
})}
|
||||
>
|
||||
<SettingsRowNote>{t('recommended_badge')}</SettingsRowNote>
|
||||
<div className="w-full min-w-0">
|
||||
<CopyBlock text={mcpUrl('claude-connector')} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
label={t('claude_code_cursor')}
|
||||
align="baseline"
|
||||
help={t('terminal_runs_browser_login')}
|
||||
>
|
||||
{/* URL is quoted: unquoted `?` in the query string trips zsh globbing. */}
|
||||
<div className="w-full min-w-0">
|
||||
<CopyBlock text={`claude mcp add ${connectorName} --transport http "${mcpUrl('claude-code')}"`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={showApiKeyMethods}
|
||||
onClick={() => setShowApiKeyMethods(!showApiKeyMethods)}
|
||||
className="flex items-center gap-2 px-1 py-3 text-xs text-muted-foreground transition-colors duration-150 hover:text-foreground"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 transition-transform duration-150',
|
||||
!showApiKeyMethods && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
{t('connect_with_api_key')}
|
||||
</button>
|
||||
<SettingsReveal open={showApiKeyMethods}>
|
||||
<div className="space-y-6 pb-3 pt-1">
|
||||
<div>
|
||||
<p className="mb-1 text-sm">Claude Desktop</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t.rich('claude_desktop_instructions', {
|
||||
code: (chunks) => <code className="text-xs">{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
<CopyBlock text={`{
|
||||
"mcpServers": {
|
||||
"${connectorName}": {
|
||||
"command": "npx",
|
||||
"args": ["gnubok-mcp"],
|
||||
"env": {
|
||||
"GNUBOK_API_KEY": "gnubok_sk_...",
|
||||
"GNUBOK_CLIENT": "claude-desktop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-1 text-sm">{t('claude_code_cursor')}</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
{t('terminal_with_api_key')}
|
||||
</p>
|
||||
<CopyBlock text={`claude mcp add ${connectorName} --transport http \\
|
||||
--url "${mcpUrl('claude-code')}" \\
|
||||
--header "Authorization: Bearer gnubok_sk_..."`} copyAriaLabel={t('copy_aria')} />
|
||||
</div>
|
||||
</div>
|
||||
</SettingsReveal>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* Create key dialog */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent className="max-w-[calc(100vw-2rem)] rounded-xl p-4 sm:max-w-3xl sm:p-6">
|
||||
|
||||
@@ -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.
|
||||
`
|
||||
@@ -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.
|
||||
`
|
||||
|
||||
+2
-1
@@ -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.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+11
-5
@@ -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 <path>Settings → Connectors → Add custom connector</path> 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 <code>~/.cursor/mcp.json</code> (global) or <code>.cursor/mcp.json</code> (per project). Cursor does not read <code>claude mcp add</code>.",
|
||||
"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 <code>claude_desktop_config.json</code> (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.",
|
||||
|
||||
+11
-5
@@ -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 <path>Settings → Connectors → Add custom connector</path> 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 <code>~/.cursor/mcp.json</code> (globalt) eller <code>.cursor/mcp.json</code> (per projekt). Cursor läser inte <code>claude mcp add</code>.",
|
||||
"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 <code>claude_desktop_config.json</code> (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.",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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.')
|
||||
|
||||
Reference in New Issue
Block a user