* 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>
6.8 KiB
paths
| paths | |||
|---|---|---|---|
|
MCP Server
Accounted exposes its bookkeeping engine as an MCP server for Claude Desktop/Code.
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, 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.
Tool namespaces: internal tool ids and authorization maps remain canonical gnubok_*. The Accounted MCP surface is explicitly selected with ?tool_namespace=accounted; it advertises accounted_* and accepts both aliases. Requests without the selector must retain the legacy server identity, catalog, and behavior.
Lazy auth (issue #1814, public-tools.ts): a client with no token may initialize, ping, list tools/prompts/resources and call the three public documentation tools (gnubok_search_tools, gnubok_list_skills, gnubok_load_skill), rate-limited per truncated IP. Every other request, i.e. the first company-scoped tools/call, answers a transport-level 401 + WWW-Authenticate: Bearer resource_metadata="..." from handleMcpRequest in server.ts (around line 18620). That challenge is what Claude.ai turns into its Connect card and Claude Code into /mcp login, and the account can be created inside it, so never answer a tokenless call with a 200 + isError. A public tool must read no tenant data, need no scope (absent from TOOL_SCOPE_MAP) and be company-independent.
Feedback + tasks: gnubok_feedback lets an agent report a missing tool, a misleading description, a wrong result or a positive signal; it emits an agent.feedback row to event_log (rate-limited 1 per 60 s per actor) and is read by the feedback-triage loop. Long-running tool calls use the MCP Tasks extension (io.modelcontextprotocol/tasks, tasks.ts): a task-capable client gets a task handle immediately and polls tasks/get; rows live in mcp_tasks (service-role writes only) so handles survive serverless instance turnover.
Tool authoring conventions (enforced by tests)
- Every
inputSchemamust declareadditionalProperties: falseat the top level. Guarded byextensions/general/mcp-server/__tests__/strict-schemas.test.ts. - Tool descriptions must be ≤ 280 chars (guarded by
output-schema.test.ts). NoArgs:/Returns:/Examples:blocks: those belong in JSON Schema, not description prose. Use agent-native hints like "Use to…" / "Call X first" instead. - Completion-signal pattern: tools that stage operations return
STAGED_OPERATION_SCHEMA:{ staged, risk_level, actor, message, preview, period_status?, next? }. Thestaged: trueboolean is the explicit completion signal; agents must not infer completion from prose. Do NOT introduce a parallel{ success, shouldContinue, output }envelope. - Machine-readable staging contract:
tools/list(andgnubok_search_toolsdetail=full) attach a derived_metato staging writes so an agent knows the contract WITHOUT reading prose.deriveToolMeta()keys offoutputSchema === STAGED_OPERATION_SCHEMAand emits{ requires_approval: true, approve_tool: 'gnubok_approve_pending_operation', preflight? }; it merges under any literal_meta(e.g. UI widget hints), which wins on collision. Add toTOOL_PREFLIGHT_MAPwhen a write has a genuine read-only pre-flight (e.g.gnubok_run_year_end→gnubok_year_end_readiness). A new staging tool inherits_metafor free: just keep its description declaring it stages (guarded by__tests__/staging-meta.test.ts).confirmed=truebelongs on the APPROVE call for high-risk ops, never on the staging tool; only some tools acceptdry_run/idempotency_key: never imply they are universal. - Skill/atom summaries:
gnubok_list_skillsandgnubok_get_agent_briefingpass registrydescriptionfields throughtoSummary()(skills/atoms.ts): the raw SKILL.md frontmatter is a long keyword-stuffed trigger list authored for CLI matching, not display copy, and gets truncated mid-sentence otherwise. Full bodies are fetched viagnubok_load_skill. The local.claude/skills/*are the Claude-Code surface; theagent_atom_registryrows seeded from the same bodies are the canonical connector surface: when they overlap, the connector atom is authoritative for MCP users. - Tools that touch a fiscal-period-bound date (categorize, mark paid, create voucher, correct/reverse entry, approve supplier invoice) pass
dateForPeriodChecktostagePendingOperationso the response includesperiod_status: { period_id, status: open|locked|closed, lock_date }. Widgets and agents use this to disable writes without round-trips. - Qualified identifiers: no bare
idin tool OUTPUT schemas: every identifier is fully qualified (transaction_id,journal_entry_id,fact_id,dimension_value_id, …) so agents never guess which entity an id belongs to. Guarded by__tests__/qualified-ids.test.ts(a shrinking grandfathered list carries the deprecatedidaliases; new tools must use qualified names only). - Error envelope: every tool failure flows through the single dispatch point (
toToolError→getStructuredError) and returns{ error: { code, message_sv, message_en, retryable, remediation? } }.retryableis ALWAYS an explicit boolean:truemeans transient (back off and retry the identical call, pairing withidempotency_keywhere the tool accepts one);falsemeans permanent for these inputs (fix arguments/state, never blind-retry). Unclassified transient failures (deadlock, statement timeout, connection drop, upstream 429/5xx) surface as codeTRANSIENT_ERROR. Don't wrap errors in ad-hoc shapes inside tools: throw (typed errors or plainError; SQLSTATE/message inference handles classification) and let the dispatch layer build the envelope. Client-side failures (e.g. the claude.ai approval elicitation's "No approval received") never reach this envelope: idempotency keys are what make those blind retries safe.