From 53d1807e8f89db546ab21df9b6ea70a9f24de69e Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:48:32 +0100 Subject: [PATCH] feat: gnubok-mcp npm package for Claude Desktop (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add gnubok-mcp npm package for Claude Desktop Published as gnubok-mcp@1.0.0 on npm. Users configure Claude Desktop with `npx gnubok-mcp` + their API key. Zero dependencies, 1.3 KB. Works as a stdio-to-HTTP bridge while Claude Desktop's OAuth connector feature is in beta. Updated settings panel instructions to show the npx approach instead of the OAuth connector flow. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: harden gnubok-mcp bridge (v1.0.1) - Rename URL → MCP_URL to avoid shadowing built-in URL constructor - Wrap non-2xx responses in JSON-RPC error instead of forwarding raw HTTP body (prevents CDN HTML pages from corrupting stdio stream) - Restore select-all on Claude Desktop config snippet Published as gnubok-mcp@1.0.1. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- components/settings/ApiKeysPanel.tsx | 17 +++- packages/gnubok-mcp/index.mjs | 124 +++++++++++++++++++++++++++ packages/gnubok-mcp/package.json | 27 ++++++ 3 files changed, 164 insertions(+), 4 deletions(-) create mode 100755 packages/gnubok-mcp/index.mjs create mode 100644 packages/gnubok-mcp/package.json diff --git a/components/settings/ApiKeysPanel.tsx b/components/settings/ApiKeysPanel.tsx index 3052f1a7..76736b0c 100644 --- a/components/settings/ApiKeysPanel.tsx +++ b/components/settings/ApiKeysPanel.tsx @@ -204,11 +204,20 @@ export function ApiKeysPanel() {

Claude Desktop

- Inställningar → Connectors → Add custom connector. Klistra in URL:en nedan. - Du loggas in automatiskt via OAuth. + Lägg till i claude_desktop_config.json (Inställningar → Developer):

-
-{mcpUrl}
+            
+{`{
+  "mcpServers": {
+    "gnubok": {
+      "command": "npx",
+      "args": ["gnubok-mcp"],
+      "env": {
+        "GNUBOK_API_KEY": "gnubok_sk_..."
+      }
+    }
+  }
+}`}
             
diff --git a/packages/gnubok-mcp/index.mjs b/packages/gnubok-mcp/index.mjs new file mode 100755 index 00000000..088432eb --- /dev/null +++ b/packages/gnubok-mcp/index.mjs @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/** + * gnubok-mcp — Connect Claude Desktop to your gnubok bookkeeping account. + * + * Usage in claude_desktop_config.json: + * { + * "mcpServers": { + * "gnubok": { + * "command": "npx", + * "args": ["gnubok-mcp"], + * "env": { + * "GNUBOK_API_KEY": "gnubok_sk_..." + * } + * } + * } + * } + * + * Get your API key at: https://app.gnubok.se/settings?tab=api + */ + +const API_KEY = process.env.GNUBOK_API_KEY +const MCP_URL = process.env.GNUBOK_URL || 'https://app.gnubok.se/api/extensions/ext/mcp-server/mcp' + +if (!API_KEY) { + process.stderr.write( + 'Error: GNUBOK_API_KEY is required.\n' + + 'Get your API key at: https://app.gnubok.se/settings?tab=api\n' + + '\n' + + 'Add it to your Claude Desktop config:\n' + + '{\n' + + ' "mcpServers": {\n' + + ' "gnubok": {\n' + + ' "command": "npx",\n' + + ' "args": ["gnubok-mcp"],\n' + + ' "env": {\n' + + ' "GNUBOK_API_KEY": "gnubok_sk_..."\n' + + ' }\n' + + ' }\n' + + ' }\n' + + '}\n' + ) + process.exit(1) +} + +let buffer = '' + +process.stdin.setEncoding('utf8') +process.stdin.on('data', (chunk) => { + buffer += chunk + + let newlineIdx + while ((newlineIdx = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, newlineIdx).trim() + buffer = buffer.slice(newlineIdx + 1) + + if (!line) continue + + handleMessage(line).catch((err) => { + process.stderr.write(`gnubok-mcp error: ${err.message}\n`) + }) + } +}) + +process.stdin.on('end', () => { + process.exit(0) +}) + +async function handleMessage(line) { + let parsed + try { + parsed = JSON.parse(line) + } catch { + process.stderr.write(`gnubok-mcp: invalid JSON\n`) + return + } + + const isNotification = parsed.id === undefined || parsed.id === null + + try { + const res = await fetch(MCP_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: line, + }) + + if (res.status === 202 || res.status === 204) { + return + } + + const text = await res.text() + + // Guard against non-JSON error responses (CDN HTML pages, proxy errors) + if (!res.ok && !isNotification) { + let message = `HTTP ${res.status}` + try { + const json = JSON.parse(text) + if (json.error) message = typeof json.error === 'string' ? json.error : JSON.stringify(json.error) + } catch { /* body wasn't JSON — use generic message */ } + const errorResponse = JSON.stringify({ + jsonrpc: '2.0', + id: parsed.id, + error: { code: -32000, message }, + }) + process.stdout.write(errorResponse + '\n') + return + } + + if (text) { + process.stdout.write(text + '\n') + } + } catch (err) { + if (!isNotification) { + const errorResponse = JSON.stringify({ + jsonrpc: '2.0', + id: parsed.id, + error: { code: -32000, message: `Connection error: ${err.message}` }, + }) + process.stdout.write(errorResponse + '\n') + } + } +} diff --git a/packages/gnubok-mcp/package.json b/packages/gnubok-mcp/package.json new file mode 100644 index 00000000..d57dcd7a --- /dev/null +++ b/packages/gnubok-mcp/package.json @@ -0,0 +1,27 @@ +{ + "name": "gnubok-mcp", + "version": "1.0.1", + "description": "Connect Claude Desktop to your gnubok bookkeeping account", + "bin": { + "gnubok-mcp": "./index.mjs" + }, + "type": "module", + "license": "MIT", + "keywords": [ + "mcp", + "gnubok", + "bookkeeping", + "claude", + "accounting" + ], + "repository": { + "type": "git", + "url": "https://github.com/erp-mafia/gnubok" + }, + "engines": { + "node": ">=18" + }, + "files": [ + "index.mjs" + ] +}