feat(mcp): allowlist Grok's connector callback and document the Grok path (#2158)

* feat(mcp): allowlist Grok's connector callback and document the Grok path

Grok custom connectors self-register through /api/mcp-oauth/register with
redirect_uri https://grok.com/connectors-oauth-exchange-code/, which the
built-in allowlist rejected with invalid_redirect_uri before consent. Add
the callback as an exact-path BUILT_IN_PATTERNS entry (trailing slash
optional, no prefix) with provider 'grok', named "Grok (xAI)" on the
consent page. Tests: accept, foreign-host and other-path rejection,
provider mapping, and a register route test for the Grok DCR shape.

Surface Grok next to ChatGPT: a "Using Grok?" side door on the onboarding
Claude step (one side door open at a time, telemetry step grok), a Grok row
under "Other clients" in the API & MCP settings tab using ?client=grok, and
sv/en strings for both. Docs: mcp-server rule, ARCHITECTURE, README,
registry entry (install section), DECISIONS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGbspj3hiNqvqTWZqdwysa
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(mcp): cite X Corp's published Grok callback, test the consent label

Review pass on #2158: the allowlist comment and DECISIONS entry claimed
xAI publishes no callback and the value came from a live observation; X
Corp lists https://grok.com/connectors-oauth-exchange-code/ as the "Grok
(web)" redirect URL at docs.x.com/x-ads-api/mcp, and grok.com serves the
path itself (slash form 308s to no-slash on the same origin). Reworded
both to cite that. Adds the consent-page test for "Grok (xAI)" next to
the ChatGPT one and a JSDoc on the onboarding side-door toggle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGbspj3hiNqvqTWZqdwysa
Signed-off-by: Emil <emilmattsson14@gmail.com>

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-09-02 14:42:39 +02:00
committed by GitHub
parent 61a76b1669
commit 6a85efb00a
14 changed files with 131 additions and 32 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ Accounted exposes its bookkeeping engine as an MCP server for Claude Desktop/Cod
**MCP extension** (`extensions/general/mcp-server/`): 150+ tools (count `name: 'gnubok_` in `server.ts`; docs say "150+", never an exact number, because it drifts) covering transactions, categorization, customers/suppliers, invoices, accounts, fiscal periods, reports (trial balance, GL, BS, IS, AR/supplier ledger, VAT, KPI), reconciliation, salary runs, AGI, year-end, document upload, and loadable skills. JSON-RPC 2.0. Endpoint: `/api/extensions/ext/mcp-server/mcp`.
**OAuth 2.1** for Claude and ChatGPT connectors: `.well-known/oauth-protected-resource` + `.well-known/oauth-authorization-server` discovery; `/api/mcp-oauth/authorize`, `/token` (PKCE), `/register`. Stateless AES-256-GCM auth codes (`lib/auth/oauth-codes.ts`). Single-use via `oauth_used_codes`. Allowlist: `claude.ai/api/*`, `claude.com/api/*`, `chatgpt.com/connector/oauth/*`, `chatgpt.com/connector_platform_oauth_redirect`, `localhost`.
**OAuth 2.1** for Claude, ChatGPT and Grok connectors: `.well-known/oauth-protected-resource` + `.well-known/oauth-authorization-server` discovery; `/api/mcp-oauth/authorize`, `/token` (PKCE), `/register`. Stateless AES-256-GCM auth codes (`lib/auth/oauth-codes.ts`). Single-use via `oauth_used_codes`. Allowlist: `claude.ai/api/*`, `claude.com/api/*`, `chatgpt.com/connector/oauth/*`, `chatgpt.com/connector_platform_oauth_redirect`, `grok.com/connectors-oauth-exchange-code/` (exact), `localhost`.
**npm packages**: `packages/accounted-mcp` is the Accounted stdio-to-HTTP bridge for new installs. `packages/gnubok-mcp` is the permanent compatibility package for existing configurations.
+1 -1
View File
@@ -112,7 +112,7 @@ transactions, draft vouchers, reconcile periods, generate reports and
declarations.
- Authentication uses scoped API keys (stored as SHA-256 hashes, rate limited
per key). Claude and ChatGPT connectors instead authenticate with OAuth 2.1
per key). Claude, ChatGPT and Grok connectors instead authenticate with OAuth 2.1
(PKCE; `app/api/mcp-oauth/{authorize,register,token}` plus the
`.well-known` discovery documents), which mints a scoped API key behind the
scenes. Authentication is lazy: a client can connect, list tools, and call a
+1
View File
@@ -1483,6 +1483,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-01] ENABLE_BANKING_SANDBOX removed from the enable-banking manifest and the index.ts header (#2131): the variable was declared as optional but never read anywhere; sandbox vs production is decided by ENABLE_BANKING_API_URL (api.tilisy.com vs api.enablebanking.com, api-client.ts derives isSandbox from the host). A dead variable declared in the manifest is what the self-hosting docs would otherwise have copied. The manifest now lists the two optional variables the code actually reads (API_URL, PSU_TYPE); the _PRODUCTION aliases stay undeclared on purpose, they are a hosted Vercel convention, not an operator contract.
[2026-09-02] Repo-wide bloat sweep (chore/bloat-sweep-2026-09): removed dead files/exports/types/i18n namespaces and deduplicated byte-identical helpers into canonical homes (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text pdfAmount/formatDateSv, lib/browser/panel-request, lib/api/v1/body + v1ValidationError, lib/bookkeeping/booking-template-schemas). Deliberately NOT done: naive Math.round(x*100)/100 helpers were not swapped for roundOre (behaviour change at half-ore values, ratchet campaign owns it); lib/bokslut/rounding.ts shim kept because money.test.ts asserts the back-compat re-export; text-based v1 body parsers (empty body allowed) kept inline because readV1JsonBody has different empty-body semantics; the four HTTP endpoints with no first-party caller (skatteverket agi/underlag + agi/sparad DELETE, invoice-inbox items/:id/history, mail connections/backfill) stay because removing a reachable endpoint is a surface change; VacationBalanceCard deleted as unreachable since #1130, the v1/MCP vacation-year-close routes stay.
[2026-09-01] PR #2130 security-scan round: the register's djuplank is validated (https + skatteverket.se host) before it is returned or navigated to, since the settings page follows it; a contested org number now WITHDRAWS an already-recorded grant nightly (not only blocks new ones), outside the downgrade guards on purpose. NOT done: proof of org-number ownership (Bolagsverket firmatecknare / BankID) before any ombud grant; the org number is tenant-editable across the product (AGI, invoices, årsredovisning) and binding it to a verified identity is a product decision for Emil, tracked as a follow-up rather than declined.
[2026-09-02] Grok custom connectors are allowlisted by the exact callback https://grok.com/connectors-oauth-exchange-code/ (trailing slash optional), not a grok.com prefix: the value is published by X Corp at docs.x.com/x-ads-api/mcp ("Grok (web)" redirect URL) and grok.com serves the path itself (slash form 308s to no-slash on the same origin), and a prefix would let any future grok.com path receive authorization codes. Grok gets side doors next to ChatGPT (onboarding) and a row under "Other clients" (settings); Claude stays the visual primary per the 2026-08-27 founder call. No new client marker plumbing: the settings URL uses the existing ?client= param with value grok.
[2026-09-02] Viewer write gate as ONE table-level trigger (enforce_company_writer_role) instead of re-emitting 15 SECURITY DEFINER bodies and ~130 policies: keyed on the JWT role claim so it fires inside definer functions too; no-op for service_role and trigger cascades. agent_conversations/agent_messages and telemetry tables deliberately excluded.
[2026-09-02] Posting-integrity guards key on current_user IN ('anon','authenticated'), not the JWT claim: inside SECURITY DEFINER RPCs current_user is the definer, so commit_journal_entry, SIE import, storno and rättelse keep working while direct PostgREST manipulation of posted vouchers is blocked. Residual: a direct draft->posted flip may still reuse an unused number below the sequence high-water mark.
[2026-09-02] Kept lib/auth/rate-limit-http.ts fail-open on hosted with an error-level log instead of failing closed: production has no Upstash configured, so fail-closed would 503 every rate-limited route until the env is set. Operator action: set UPSTASH_REDIS_REST_URL/TOKEN.
+1 -1
View File
@@ -35,7 +35,7 @@ Open-source Swedish accounting software for sole traders (enskild firma) and lim
- **Skattekonto** -- Tax account transactions synced from Skatteverket or imported from statement files, linked to the booked 1630 movements for reconciliation
- **Document archive** -- SHA-256 integrity, 7-year retention enforcement, full archive ZIP export
- **SIE import/export** -- Standard Swedish accounting interchange format
- **Agent access (MCP)** -- 150+ bookkeeping tools over the Model Context Protocol, with scoped API keys and staged approvals
- **Agent access (MCP)** -- 150+ bookkeeping tools over the Model Context Protocol, with scoped API keys and staged approvals; connects to Claude, ChatGPT and Grok over OAuth 2.1
- **Claude connector and plugin** -- Connect Claude.ai or Claude Code over OAuth 2.1 and install approval-gated workflow skills (`/accounted:bookkeep`, `/accounted:vat`, `/accounted:year-end`, ...) from [claude-plugin/](claude-plugin/README.md)
- **Extension system** -- Opt-in plugins for AI categorization, receipt OCR, email, calendar, and more
@@ -40,6 +40,7 @@ import { GET, POST } from '../route'
const CLAUDE: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'claude' }
const CHATGPT: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'chatgpt' }
const GROK: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'grok' }
const REGISTERED: RedirectUriResolution = {
allowed: true,
kind: 'registered',
@@ -364,6 +365,25 @@ describe('client identity on the consent page', () => {
expect(html).toContain('chatgpt.com')
})
it('names Grok as a verified client for the grok.com callback', async () => {
mocks.resolveRedirectUri.mockResolvedValue(GROK)
const html = await (
await GET(
new Request(
buildAuthorizeUrl({
...params,
redirect_uri: 'https://grok.com/connectors-oauth-exchange-code/',
}),
),
)
).text()
expect(html).toContain('Grok (xAI)')
expect(html).toContain('Verifierad')
expect(html).toContain('grok.com')
expect(html).not.toContain('En extern applikation')
})
it('shows client_name and redirect host for a DB-registered client, never marked verified', async () => {
mocks.resolveRedirectUri.mockResolvedValue(REGISTERED)
const html = await (
+2
View File
@@ -1092,6 +1092,8 @@ function describeClient(
return { name: 'Claude (Anthropic)', tag: 'Verifierad', verified: true }
case 'chatgpt':
return { name: 'ChatGPT (OpenAI)', tag: 'Verifierad', verified: true }
case 'grok':
return { name: 'Grok (xAI)', tag: 'Verifierad', verified: true }
case 'local':
return { name: 'Lokal utveckling (localhost)', tag: 'Din egen dator', verified: false }
}
@@ -51,6 +51,25 @@ describe('POST /api/mcp-oauth/register', () => {
expect(response.status).toBe(201)
})
it('accepts registration with the grok.com connector callback', async () => {
const response = await POST(createRequest({
client_name: 'Grok',
redirect_uris: ['https://grok.com/connectors-oauth-exchange-code/'],
token_endpoint_auth_method: 'none',
}))
expect(response.status).toBe(201)
const body = await response.json()
expect(body.redirect_uris).toEqual(['https://grok.com/connectors-oauth-exchange-code/'])
expect(body.token_endpoint_auth_method).toBe('none')
})
it('rejects other grok.com paths', async () => {
const response = await POST(createRequest({
redirect_uris: ['https://grok.com/oauth/callback'],
}))
expect(response.status).toBe(400)
})
it('rejects registration with disallowed redirect_uris', async () => {
const response = await POST(createRequest({
redirect_uris: ['https://evil.com/callback'],
+35 -20
View File
@@ -47,6 +47,12 @@ interface NewUserChecklistProps {
sieSweep?: { auto_linked: number; suggested: number; unmatched: number; errors: number } | null
}
/** Clients that get a collapsed "Using X?" side door under the Claude step.
* Each value keys the i18n strings step_claude_<door>_link / _steps and the
* telemetry step name. Order is display order. */
const SIDE_DOORS = ['chatgpt', 'grok'] as const
type SideDoor = (typeof SIDE_DOORS)[number]
/**
* Activation funnel events, mirroring the one existing product-event site
* (lib/support/submit-feedback.ts): guarded, try/caught, no PII in
@@ -108,9 +114,9 @@ export default function NewUserChecklist({
// effect and re-raised the error toast forever. The next visit tries once
// more from server truth.
const completeRejectedRef = useRef(false)
// The ChatGPT side door on the Claude step: collapsed by default so the
// one-click Claude path stays the visual primary.
const [chatGptOpen, setChatGptOpen] = useState(false)
// The ChatGPT and Grok side doors on the Claude step: collapsed by default
// so the one-click Claude path stays the visual primary; at most one open.
const [sideDoor, setSideDoor] = useState<SideDoor | null>(null)
const [serverUrlCopied, setServerUrlCopied] = useState(false)
const hasMigration = ENABLED_EXTENSION_IDS.has('arcim-migration')
@@ -258,12 +264,16 @@ export default function NewUserChecklist({
'noopener',
)
}
// ChatGPT has no add-connector deep link (the user pastes the server URL
// into Developer mode manually), so the side door copies the URL instead.
const toggleChatGpt = () => {
setChatGptOpen((open) => {
if (!open) captureSetup('onboarding_setup_step_started', { step: 'chatgpt' })
return !open
/**
* Open one side door (closing any other) or close it when it is already
* open. Neither ChatGPT nor Grok has an add-connector deep link (the user
* pastes the server URL into the client manually), so the side doors copy
* the URL instead. Telemetry fires once per open, never on close.
*/
const toggleSideDoor = (door: SideDoor) => {
setSideDoor((open) => {
if (open !== door) captureSetup('onboarding_setup_step_started', { step: door })
return open === door ? null : door
})
}
const copyServerUrl = async () => {
@@ -467,18 +477,23 @@ export default function NewUserChecklist({
{t('step_claude_guide_link')}
</a>
</p>
<button
type="button"
onClick={toggleChatGpt}
aria-expanded={chatGptOpen}
className="text-xs text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
>
{t('step_claude_chatgpt_link')}
</button>
{chatGptOpen && (
<div className="flex flex-wrap gap-x-4 gap-y-1">
{SIDE_DOORS.map((door) => (
<button
key={door}
type="button"
onClick={() => toggleSideDoor(door)}
aria-expanded={sideDoor === door}
className="text-xs text-muted-foreground underline decoration-border underline-offset-4 transition-colors hover:text-foreground"
>
{t(`step_claude_${door}_link`)}
</button>
))}
</div>
{sideDoor && (
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
<p className="max-w-prose text-xs leading-5 text-muted-foreground">
{t('step_claude_chatgpt_steps', { appName })}
{t(`step_claude_${sideDoor}_steps`, { appName })}
</p>
<Button size="sm" variant="outline" onClick={() => void copyServerUrl()}>
{serverUrlCopied
@@ -533,7 +548,7 @@ function Step({
doneNote?: React.ReactNode
/** Block content below the pitch while the step is open. Unlike `children`
* (which lives inside a <p>), this may hold nested block elements, e.g.
* the Claude step's ChatGPT side door. */
* the Claude step's ChatGPT and Grok side doors. */
footnote?: React.ReactNode
last?: boolean
children: React.ReactNode
+10
View File
@@ -422,6 +422,16 @@ export function ApiKeysPanel() {
<CopyBlock text={mcpUrl('claude-connector')} copyAriaLabel={t('copy_aria')} />
</div>
<div>
<p className="mb-1 text-sm">Grok</p>
<p className="mb-2 text-xs text-muted-foreground">
{t.rich('grok_instructions', {
path: (chunks) => <strong>{chunks}</strong>,
})}
</p>
<CopyBlock text={mcpUrl('grok')} 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>
@@ -19,6 +19,13 @@ describe('isBuiltInRedirectUri', () => {
['https://chatgpt.com/connector_platform_oauth_redirect/extra', false],
['https://chatgpt.com/other/path', false],
['https://chatgpt.com.evil.com/connector/oauth/x', false],
['https://grok.com/connectors-oauth-exchange-code/', true],
['https://grok.com/connectors-oauth-exchange-code', true],
['https://grok.com/connectors-oauth-exchange-code/extra', false],
['https://grok.com/connectors-oauth-exchange-code/?next=x', false],
['https://grok.com/other/path', false],
['https://grok.com.evil.com/connectors-oauth-exchange-code/', false],
['http://grok.com/connectors-oauth-exchange-code/', false],
['http://localhost:3000/cb', true],
['http://localhost/cb', true],
['http://127.0.0.1:8080/cb', true],
@@ -37,6 +44,8 @@ describe('builtInRedirectProvider', () => {
['https://claude.com/api/oauth/callback', 'claude'],
['https://chatgpt.com/connector/oauth/abc123', 'chatgpt'],
['https://chatgpt.com/connector_platform_oauth_redirect', 'chatgpt'],
['https://grok.com/connectors-oauth-exchange-code/', 'grok'],
['https://grok.com/connectors-oauth-exchange-code/extra', null],
['http://localhost:3000/cb', 'local'],
['http://127.0.0.1:8080/cb', 'local'],
['https://claude-login.example/cb', null],
+13 -3
View File
@@ -14,25 +14,35 @@ import { scopeKind, type ApiKeyScope } from './scope-catalog'
/**
* Identity of a built-in client, derived from the redirect URI pattern that
* matched. Rendered on the consent page so the user can tell a real Claude /
* ChatGPT connector from a look-alike registration.
* ChatGPT / Grok connector from a look-alike registration.
*/
export type BuiltInProvider = 'claude' | 'chatgpt' | 'local'
export type BuiltInProvider = 'claude' | 'chatgpt' | 'grok' | 'local'
/**
* Built-in redirect URI patterns. These bypass the DB lookup entirely so
* Claude's and ChatGPT's connectors keep working without seeded rows, and so
* the Claude, ChatGPT and Grok connectors keep working without seeded rows, and so
* local development never depends on having a registration.
*
* ChatGPT uses a per-connector-instance callback path
* (https://chatgpt.com/connector/oauth/{callback_id}) plus the legacy fixed
* callback for already-published apps; both are documented at
* developers.openai.com/apps-sdk/build/auth.
*
* Grok (grok.com custom connectors) registers itself through /register as a
* public client and sends a single fixed callback,
* https://grok.com/connectors-oauth-exchange-code/, published by X Corp as
* the "Grok (web)" redirect URL at docs.x.com/x-ads-api/mcp (xAI's own
* connector docs at docs.x.ai do not state it). grok.com serves the path
* itself: the slash form 308s to the no-slash form on the same origin, so
* both are accepted. Matched as an exact path, never a prefix, so a future
* grok.com path cannot ride on this entry.
*/
const BUILT_IN_PATTERNS: readonly { pattern: RegExp; provider: BuiltInProvider }[] = [
{ pattern: /^https:\/\/claude\.ai\/api\//, provider: 'claude' },
{ pattern: /^https:\/\/claude\.com\/api\//, provider: 'claude' },
{ pattern: /^https:\/\/chatgpt\.com\/connector\/oauth\//, provider: 'chatgpt' },
{ pattern: /^https:\/\/chatgpt\.com\/connector_platform_oauth_redirect$/, provider: 'chatgpt' },
{ pattern: /^https:\/\/grok\.com\/connectors-oauth-exchange-code\/?$/, provider: 'grok' },
{ pattern: /^http:\/\/localhost(:\d+)?(\/|$)/, provider: 'local' },
{ pattern: /^http:\/\/127\.0\.0\.1(:\d+)?(\/|$)/, provider: 'local' },
]
+5 -2
View File
@@ -1358,6 +1358,8 @@
"step_claude_chatgpt_steps": "In ChatGPT (Plus, Pro or Business): Settings → Apps → Advanced settings → turn on Developer mode. Choose Add custom connector, name it {appName} and paste the server address. Then start a new chat and ask for a walkthrough of your books.",
"step_claude_chatgpt_copy": "Copy server address",
"step_claude_chatgpt_copied": "Copied",
"step_claude_grok_link": "Using Grok?",
"step_claude_grok_steps": "On grok.com: Connectors → New Connector → Custom. Name it {appName}, paste the server address and sign in when Grok asks. Then start a new chat with the connector switched on and ask for a walkthrough of your books.",
"step_claude_expectation": "Claude lists the tools straight away. The first real question opens a login prompt: approve it and ask again.",
"step_claude_guide_link": "Guide: connect Claude step by step"
},
@@ -2600,10 +2602,11 @@
"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. Claude lists the tools straight away; the first real question opens a login prompt, approve it and ask again.",
"other_clients": "Other clients: Claude Code, Cursor, plugin",
"other_clients": "Other clients: Grok, Claude Code, Cursor, plugin",
"claude_ai_manual": "claude.ai (manual)",
"works_with_ai": "Works with Claude, ChatGPT, and other AI assistants.",
"works_with_ai": "Works with Claude, ChatGPT, Grok, and other AI assistants.",
"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.",
"grok_instructions": "On grok.com go to <path>Connectors → New Connector → Custom</path> and paste the MCP server URL. Grok registers itself and opens the sign-in: no API key needed.",
"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",
+5 -2
View File
@@ -1358,6 +1358,8 @@
"step_claude_chatgpt_steps": "I ChatGPT (Plus, Pro eller Business): Inställningar → Appar → Avancerade inställningar → slå på Utvecklarläge. Välj Lägg till anpassad connector, namnge den {appName} och klistra in serveradressen. Starta sedan en ny chatt och be om en genomgång av bokföringen.",
"step_claude_chatgpt_copy": "Kopiera serveradress",
"step_claude_chatgpt_copied": "Kopierad",
"step_claude_grok_link": "Använder du Grok?",
"step_claude_grok_steps": "På grok.com: Connectors → New Connector → Custom. Namnge den {appName}, klistra in serveradressen och logga in när Grok frågar. Starta sedan en ny chatt med connectorn påslagen och be om en genomgång av bokföringen.",
"step_claude_expectation": "Claude listar verktygen direkt. Första riktiga frågan öppnar en inloggning: godkänn den och ställ frågan igen.",
"step_claude_guide_link": "Guide: anslut Claude steg för steg"
},
@@ -2600,10 +2602,11 @@
"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. Claude listar verktygen direkt; första riktiga frågan öppnar en inloggning, godkänn den och ställ frågan igen.",
"other_clients": "Andra klienter: Claude Code, Cursor, plugin",
"other_clients": "Andra klienter: Grok, Claude Code, Cursor, plugin",
"claude_ai_manual": "claude.ai (manuellt)",
"works_with_ai": "Fungerar med Claude, ChatGPT och andra AI-assistenter.",
"works_with_ai": "Fungerar med Claude, ChatGPT, Grok och andra AI-assistenter.",
"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.",
"grok_instructions": "På grok.com: gå till <path>Connectors → New Connector → Custom</path> och klistra in MCP-serverns URL. Grok registrerar sig själv och öppnar inloggningen: ingen API-nyckel behövs.",
"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",
+9 -2
View File
@@ -1,6 +1,6 @@
---
title: "accounted MCP-server"
description: "MCP-server för svensk dubbel bokföring. Över 150 verktyg över bokföring, fakturor, leverantörer, moms, lön och rapporter. OAuth 2.1 med PKCE och dynamisk klientregistrering. Installeras som connector i Claude.ai, Claude Desktop, Cursor och Continue."
description: "MCP-server för svensk dubbel bokföring. Över 150 verktyg över bokföring, fakturor, leverantörer, moms, lön och rapporter. OAuth 2.1 med PKCE och dynamisk klientregistrering. Installeras som connector i Claude.ai, ChatGPT, Grok, Claude Desktop, Cursor och Continue."
slug: "gnubok-mcp"
kind: "mcp"
author: "gnubok"
@@ -31,7 +31,7 @@ faq:
- q: "Hur funkar OAuth-flödet?"
a: "MCP-klienten upptäcker accounteds autentiseringsserver via `.well-known/oauth-protected-resource`. Klienten registrerar sig dynamiskt (RFC 7591) på `/api/mcp-oauth/register`, öppnar `/api/mcp-oauth/authorize` i webbläsaren, du loggar in på accounted och godkänner anslutningen, klienten utbyter authorization code mot en access token via `/api/mcp-oauth/token` med PKCE S256."
- q: "Vilka klienter funkar?"
a: "Allt som följer MCP-spec 2026-02 och stödjer HTTP-transport med OAuth: Claude.ai (connectors), Claude Desktop (HTTP), Cursor, Continue, Codex. För stdio-bara klienter finns en lokal bro: `npx gnubok-mcp` med API-nyckel."
a: "Allt som följer MCP-spec 2026-02 och stödjer HTTP-transport med OAuth: Claude.ai (connectors), ChatGPT (Developer mode), Grok (custom connectors), Claude Desktop (HTTP), Cursor, Continue, Codex. För stdio-bara klienter finns en lokal bro: `npx gnubok-mcp` med API-nyckel."
- q: "Vilka scopes ger jag bort?"
a: "OAuth-flödet ger en enskild `mcp`-scope: 'denna agent får använda accounteds MCP'. Inom det är agentens åtkomst begränsad till samma rättigheter som ditt eget accounted-konto har. Du kan när som helst återkalla anslutningen i app.gnubok.se → Inställningar."
- q: "Får agenten skriva direkt till huvudboken?"
@@ -69,6 +69,13 @@ verktyg den får exponera.
3. Claude öppnar accounted-OAuth i webbläsaren. Logga in. Godkänn anslutningen.
4. Connectorn dyker upp i listan. Slå på för de chattar där du vill ha den aktiv.
## Installera i Grok
1. På grok.com: Connectors → New Connector → Custom.
2. Ange URL: `https://app.gnubok.se/api/extensions/ext/mcp-server/mcp`.
3. Grok registrerar sig själv och öppnar accounted-OAuth. Logga in. Godkänn anslutningen.
4. Starta en ny chatt med connectorn påslagen.
## Installera i Claude Desktop
`claude_desktop_config.json` (`~/Library/Application Support/Claude/` på macOS):