@hoagsmedia/frgur-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +83 -0
  2. package/index.mjs +77 -0
  3. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @hoagsmedia/frgur-mcp-server
2
+
3
+ A thin, **read-only** [MCP](https://modelcontextprotocol.io) bridge to your
4
+ [frugr](https://frgur.com) books. Point your own AI client — Cursor, Claude
5
+ Desktop, Claude Code, or any MCP-capable app — at your accounting data and ask
6
+ questions in plain language.
7
+
8
+ ## What it does
9
+
10
+ It exposes three read-only tools over stdio and forwards each call to the frugr
11
+ API using your Personal Access Token:
12
+
13
+ | Tool | Returns |
14
+ | --------------------- | --------------------------------------------------------- |
15
+ | `get_profit_loss` | Income, expenses, net, and per-category totals for a year |
16
+ | `search_transactions` | Ledger transactions by text, date range, and/or category |
17
+ | `get_open_invoices` | Outstanding receivables with balances by client |
18
+
19
+ The bridge holds no credentials of its own, **never writes** to your books, and
20
+ runs **your** AI client's model under **your** subscription — frugr is only the
21
+ data provider. Your token scopes access to a single company and can be revoked
22
+ at any time.
23
+
24
+ ## Setup
25
+
26
+ 1. In frugr, go to **Settings → External AI apps (MCP)** and create a token
27
+ (owner/admin only). Copy it — it's shown once.
28
+ 2. Add the bridge to your client's MCP config:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "frgur": {
34
+ "command": "npx",
35
+ "args": ["-y", "@hoagsmedia/frgur-mcp-server"],
36
+ "env": {
37
+ "FRGUR_API_URL": "https://app.frgur.com",
38
+ "FRGUR_API_TOKEN": "frgur_pat_your_token_here"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ - **Claude Desktop:** Settings → Developer → Edit Config.
46
+ - **Cursor:** `.cursor/mcp.json` in your project (or the global equivalent).
47
+ - **Claude Code:** `claude mcp add` or your `mcp.json`.
48
+
49
+ Requires Node.js 18+.
50
+
51
+ ## Environment
52
+
53
+ | Variable | Required | Default | Notes |
54
+ | ----------------- | -------- | ----------------------- | -------------------------- |
55
+ | `FRGUR_API_TOKEN` | yes | — | Your Personal Access Token |
56
+ | `FRGUR_API_URL` | no | `https://app.frgur.com` | Your frugr origin |
57
+
58
+ ## Verifying a token
59
+
60
+ ```sh
61
+ curl -H "Authorization: Bearer frgur_pat_…" https://app.frgur.com/api/mcp
62
+ # → {"ok":true,"transport":"frgur-mcp","tools":["get_profit_loss", ...]}
63
+ ```
64
+
65
+ ## Security
66
+
67
+ - Read-only: the only reachable code path queries your books; nothing writes.
68
+ - Org-scoped: a token resolves to exactly one company server-side; the tools
69
+ never accept a tenant id from the client.
70
+ - Rate-limited and audited per token on the frugr side.
71
+ - Revoke a token instantly from Settings → External AI apps (MCP).
72
+
73
+ ## Publishing (maintainers)
74
+
75
+ Package: `@hoagsmedia/frgur-mcp-server` under the [hoagsmedia](https://www.npmjs.com/org/hoagsmedia) npm org.
76
+
77
+ 1. Bump `version` in `mcp-server/package.json`.
78
+ 2. Merge to `main`, then either:
79
+ - **GitHub Actions:** run workflow **Publish MCP server** (`workflow_dispatch`), or
80
+ - **Tag:** push `frgur-mcp-server-vX.Y.Z` (also triggers publish).
81
+ 3. Requires `NPM_TOKEN` in repo secrets — use an npm **Granular Access Token** with type **Automation** (publish to `@hoagsmedia`). Legacy/read-write tokens with account 2FA fail CI with `EOTP`; automation tokens skip OTP.
82
+
83
+ Local dry run: `npm publish --dry-run` from `mcp-server/`.
package/index.mjs ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // @hoagsmedia/frgur-mcp-server — a thin, READ-ONLY MCP bridge to your frugr books.
3
+ //
4
+ // It exposes frugr's books tools over stdio (for Cursor / Claude Desktop /
5
+ // Claude Code / ChatGPT) and forwards every call to the frugr API using your
6
+ // Personal Access Token. The bridge holds no credentials of its own, runs your
7
+ // own AI client's model (not frugr's), and can only read — it never writes.
8
+ //
9
+ // Config (env):
10
+ // FRGUR_API_TOKEN required — a token from frugr → Settings → External AI apps
11
+ // FRGUR_API_URL optional — your frugr origin (default https://app.frgur.com)
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+
16
+ const API_URL = (process.env.FRGUR_API_URL ?? 'https://app.frgur.com').replace(/\/+$/, '');
17
+ const API_TOKEN = process.env.FRGUR_API_TOKEN ?? '';
18
+ const ENDPOINT = `${API_URL}/api/mcp`;
19
+
20
+ // Diagnostics MUST go to stderr — stdout is the MCP protocol channel.
21
+ function logErr(msg) {
22
+ process.stderr.write(`[frgur-mcp] ${msg}\n`);
23
+ }
24
+
25
+ if (!API_TOKEN) {
26
+ logErr('FRGUR_API_TOKEN is required. Create one in frugr → Settings → External AI apps (MCP).');
27
+ process.exit(1);
28
+ }
29
+
30
+ /** POST one bridge request to the frugr endpoint, authenticated by the PAT. */
31
+ async function call(method, params) {
32
+ let res;
33
+ try {
34
+ res = await fetch(ENDPOINT, {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${API_TOKEN}` },
37
+ body: JSON.stringify({ method, params })
38
+ });
39
+ } catch (e) {
40
+ throw new Error(`Could not reach frugr at ${ENDPOINT}: ${e?.message ?? e}`, { cause: e });
41
+ }
42
+ const text = await res.text();
43
+ let data;
44
+ try {
45
+ data = text ? JSON.parse(text) : {};
46
+ } catch {
47
+ data = { error: text };
48
+ }
49
+ if (!res.ok) throw new Error(data?.error ?? `frugr API error ${res.status}`);
50
+ return data;
51
+ }
52
+
53
+ const server = new Server(
54
+ { name: 'frgur-books', version: '0.1.0' },
55
+ { capabilities: { tools: {} } }
56
+ );
57
+
58
+ // Tool list comes straight from frugr, so adding a tool server-side surfaces it
59
+ // to every client with no package update.
60
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
61
+ const { tools } = await call('tools/list');
62
+ return { tools: tools ?? [] };
63
+ });
64
+
65
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
66
+ const { name, arguments: args } = request.params;
67
+ const result = await call('tools/call', { name, arguments: args ?? {} });
68
+ // frugr already returns an MCP CallTool result ({ content, isError }); pass it
69
+ // through, falling back to stringifying anything unexpected.
70
+ return {
71
+ content: result.content ?? [{ type: 'text', text: JSON.stringify(result) }],
72
+ isError: Boolean(result.isError)
73
+ };
74
+ });
75
+
76
+ await server.connect(new StdioServerTransport());
77
+ logErr(`connected to ${API_URL} — read-only books tools available.`);
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@hoagsmedia/frgur-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Read-only MCP bridge to your frugr books — profit & loss, transaction search, open invoices. Bring your own AI client (Cursor, Claude Desktop, ChatGPT).",
5
+ "type": "module",
6
+ "bin": {
7
+ "frgur-mcp": "index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.29.0"
22
+ }
23
+ }