@hoagsmedia/frgur-mcp-server 0.1.0 → 0.3.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 (4) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +53 -20
  3. package/index.mjs +103 -10
  4. package/package.json +8 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 — 2026-07-06
4
+
5
+ - **Import receipts from a local file (HOA-416).** New client-side tool `import_receipt_file({ filePath })`: reads a receipt (PDF/JPEG/PNG/WebP/GIF/HEIC, ≤3 MB) from your machine, base64s it, and forwards to frugr's new `propose_expense_from_receipt` server tool. frugr extracts the receipt and either attaches it to the matching unreceipted expense or files a provisional draft in **Needs Review** — never a direct ledger write. Ask your AI client: _"import ~/Downloads/receipt.pdf into my books."_
6
+ - The local tool is advertised only when the connected frugr server actually offers receipt import, so the bridge stays compatible with older deployments.
7
+ - Bridge is no longer strictly read-only (it exposes frugr's review-queue "propose" tools); description/docs updated. Approval always happens in the frugr app.
8
+
9
+ ## 0.2.0 — 2026-07-02
10
+
11
+ - Document all **12** read-only books tools (was marketed as 3 in the npm description).
12
+ - Pin-friendly Settings wizard config: `@hoagsmedia/frugr-mcp@0.2.0` (legacy `@hoagsmedia/frgur-mcp-server@0.2.0` still works).
13
+ - Publish branding alias **`@hoagsmedia/frugr-mcp`** (same stdio bridge; `frugr-mcp` bin).
14
+ - **`@hoagsmedia/frgur-mcp-server`** remains supported; `FRGUR_API_TOKEN` / `FRGUR_API_URL` unchanged.
15
+
16
+ ## 0.1.0 — 2026-06-23
17
+
18
+ - Initial npm publish (HOA-299): stdio MCP bridge to frugr read-only books tools.
package/README.md CHANGED
@@ -1,20 +1,31 @@
1
1
  # @hoagsmedia/frgur-mcp-server
2
2
 
3
+ > **Branding:** the preferred npm name is now [`@hoagsmedia/frugr-mcp`](https://www.npmjs.com/package/@hoagsmedia/frugr-mcp) — same package, same env vars. `@hoagsmedia/frgur-mcp-server` remains supported for existing configs.
4
+
3
5
  A thin, **read-only** [MCP](https://modelcontextprotocol.io) bridge to your
4
- [frugr](https://frgur.com) books. Point your own AI client — Cursor, Claude
6
+ [frugr](https://frugr.app) books. Point your own AI client — Cursor, Claude
5
7
  Desktop, Claude Code, or any MCP-capable app — at your accounting data and ask
6
8
  questions in plain language.
7
9
 
8
10
  ## What it does
9
11
 
10
- It exposes three read-only tools over stdio and forwards each call to the frugr
12
+ It exposes read-only tools over stdio and forwards each call to the frugr
11
13
  API using your Personal Access Token:
12
14
 
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 |
15
+ | Tool | Returns |
16
+ | --------------------------------------- | ---------------------------------------------------------- |
17
+ | `get_profit_loss` | Income, expenses, net, and per-category totals for a year |
18
+ | `search_transactions` | Ledger transactions by text, date range, and/or category |
19
+ | `list_categories` | Chart-of-accounts category names and kinds |
20
+ | `summarize_transactions` | Transaction totals, rollups, and largest matching rows |
21
+ | `compare_periods` | Period-over-period transaction totals and counts |
22
+ | `compare_receipts_to_bank_transactions` | Receipt-backed rows vs matching bank rows |
23
+ | `get_open_invoices` | Outstanding receivables with balances by client |
24
+ | `get_open_bills` | Outstanding payables with due dates and vendor balances |
25
+ | `get_reconciliation_status` | Account reconciliation freshness and uncleared counts |
26
+ | `get_time_mileage_summary` | Billable time and mileage trip summaries |
27
+ | `get_business_snapshot` | Compact receivables, payables, spend, and cleanup snapshot |
28
+ | `find_bookkeeping_issues` | Uncategorized rows, missing receipts, and duplicates |
18
29
 
19
30
  The bridge holds no credentials of its own, **never writes** to your books, and
20
31
  runs **your** AI client's model under **your** subscription — frugr is only the
@@ -32,9 +43,9 @@ at any time.
32
43
  "mcpServers": {
33
44
  "frgur": {
34
45
  "command": "npx",
35
- "args": ["-y", "@hoagsmedia/frgur-mcp-server"],
46
+ "args": ["-y", "@hoagsmedia/frugr-mcp@0.2.0"],
36
47
  "env": {
37
- "FRGUR_API_URL": "https://app.frgur.com",
48
+ "FRGUR_API_URL": "https://frugr.app",
38
49
  "FRGUR_API_TOKEN": "frgur_pat_your_token_here"
39
50
  }
40
51
  }
@@ -48,17 +59,19 @@ at any time.
48
59
 
49
60
  Requires Node.js 18+.
50
61
 
62
+ Prefer the **`frugr`** spelling? Use `@hoagsmedia/frugr-mcp@0.2.0` instead — identical bridge.
63
+
51
64
  ## Environment
52
65
 
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 |
66
+ | Variable | Required | Default | Notes |
67
+ | ----------------- | -------- | ------------------- | -------------------------- |
68
+ | `FRGUR_API_TOKEN` | yes | — | Your Personal Access Token |
69
+ | `FRGUR_API_URL` | no | `https://frugr.app` | Your frugr origin |
57
70
 
58
71
  ## Verifying a token
59
72
 
60
73
  ```sh
61
- curl -H "Authorization: Bearer frgur_pat_…" https://app.frgur.com/api/mcp
74
+ curl -H "Authorization: Bearer frgur_pat_…" https://frugr.app/api/mcp
62
75
  # → {"ok":true,"transport":"frgur-mcp","tools":["get_profit_loss", ...]}
63
76
  ```
64
77
 
@@ -72,12 +85,32 @@ curl -H "Authorization: Bearer frgur_pat_…" https://app.frgur.com/api/mcp
72
85
 
73
86
  ## Publishing (maintainers)
74
87
 
75
- Package: `@hoagsmedia/frgur-mcp-server` under the [hoagsmedia](https://www.npmjs.com/org/hoagsmedia) npm org.
88
+ Packages (same `index.mjs`, publish both on release):
89
+
90
+ | npm name | Bin |
91
+ | ------------------------------ | ----------- |
92
+ | `@hoagsmedia/frugr-mcp` | `frugr-mcp` |
93
+ | `@hoagsmedia/frgur-mcp-server` | `frgur-mcp` |
76
94
 
77
- 1. Bump `version` in `mcp-server/package.json`.
78
- 2. Merge to `main`, then either:
95
+ Under the [hoagsmedia](https://www.npmjs.com/org/hoagsmedia) npm org.
96
+
97
+ 1. Bump `version` in `mcp-server/package.json` and add a `CHANGELOG.md` entry.
98
+ 2. Run `npm run sync-frugr-alias` in `mcp-server/` (also runs on `prepublishOnly`; CI fails if committed alias copies drift).
99
+ 3. Merge to `main`, then **publish before** the pinned Settings copy reaches prod:
79
100
  - **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.
101
+ - **Tag:** push `frgur-mcp-server-vX.Y.Z` (publishes both packages).
102
+ 4. 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.
103
+
104
+ Local dry run:
82
105
 
83
- Local dry run: `npm publish --dry-run` from `mcp-server/`.
106
+ ```sh
107
+ cd mcp-server && npm publish --dry-run
108
+ cd mcp-server/frugr-alias && npm publish --dry-run
109
+ ```
110
+
111
+ Smoke after publish (stderr banner; needs a valid PAT — or use Settings → Test connection):
112
+
113
+ ```sh
114
+ FRGUR_API_TOKEN=frgur_pat_… npx -y @hoagsmedia/frugr-mcp@X.Y.Z 2>&1 | head -1
115
+ # → [frgur-mcp] connected to …
116
+ ```
package/index.mjs CHANGED
@@ -1,19 +1,31 @@
1
1
  #!/usr/bin/env node
2
- // @hoagsmedia/frgur-mcp-server — a thin, READ-ONLY MCP bridge to your frugr books.
2
+ // @hoagsmedia/frgur-mcp-server — a thin MCP bridge to your frugr books.
3
3
  //
4
4
  // It exposes frugr's books tools over stdio (for Cursor / Claude Desktop /
5
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.
6
+ // Personal Access Token. The bridge holds no credentials of its own and runs
7
+ // your own AI client's model (not frugr's). Most tools READ your books; the
8
+ // "propose" tools (incl. receipt import) only ever stage suggestions into
9
+ // frugr's Needs Review queue — nothing writes to your ledger without your
10
+ // approval in the app.
8
11
  //
9
12
  // Config (env):
10
13
  // 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)
14
+ // FRGUR_API_URL optional — your frugr origin (default https://frugr.app)
12
15
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
17
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
18
+ import { readFileSync, statSync } from 'node:fs';
19
+ import { readFile } from 'node:fs/promises';
20
+ import { homedir } from 'node:os';
21
+ import { basename, dirname, extname, join, resolve } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
15
23
 
16
- const API_URL = (process.env.FRGUR_API_URL ?? 'https://app.frgur.com').replace(/\/+$/, '');
24
+ const PKG = JSON.parse(
25
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
26
+ );
27
+
28
+ const API_URL = (process.env.FRGUR_API_URL ?? 'https://frugr.app').replace(/\/+$/, '');
17
29
  const API_TOKEN = process.env.FRGUR_API_TOKEN ?? '';
18
30
  const ENDPOINT = `${API_URL}/api/mcp`;
19
31
 
@@ -50,21 +62,102 @@ async function call(method, params) {
50
62
  return data;
51
63
  }
52
64
 
65
+ // --- Local file bridge for receipt import (v0.3.0) --------------------------
66
+ // Claude Desktop can't turn a chat attachment into base64, but this bridge runs
67
+ // on the user's machine and can read files. `import_receipt_file` is a
68
+ // CLIENT-SIDE tool: it reads the path locally and forwards the bytes to the
69
+ // server's `propose_expense_from_receipt`.
70
+ const RECEIPT_MAX_BYTES = 3 * 1024 * 1024;
71
+ const RECEIPT_MIME_BY_EXT = {
72
+ '.pdf': 'application/pdf',
73
+ '.jpg': 'image/jpeg',
74
+ '.jpeg': 'image/jpeg',
75
+ '.png': 'image/png',
76
+ '.webp': 'image/webp',
77
+ '.gif': 'image/gif',
78
+ '.heic': 'image/heic'
79
+ };
80
+
81
+ export function expandPath(p) {
82
+ const raw = String(p ?? '').trim();
83
+ if (!raw) return '';
84
+ const expanded = raw === '~' ? homedir() : raw.replace(/^~(?=\/|\\)/, homedir());
85
+ return resolve(expanded);
86
+ }
87
+
88
+ export function mimeFromExtension(filePath) {
89
+ return RECEIPT_MIME_BY_EXT[extname(filePath).toLowerCase()] ?? null;
90
+ }
91
+
92
+ const IMPORT_RECEIPT_TOOL = {
93
+ name: 'import_receipt_file',
94
+ description:
95
+ 'Import a receipt from a file on THIS computer (e.g. ~/Downloads/receipt.pdf). Reads the ' +
96
+ 'file locally and proposes it to frugr, which files it in the Needs Review queue for your ' +
97
+ 'approval. Max 3 MB; PDF, JPEG, PNG, WebP, GIF, or HEIC.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ filePath: {
102
+ type: 'string',
103
+ description: 'Absolute or ~-relative path to the receipt file on this machine.'
104
+ }
105
+ },
106
+ required: ['filePath']
107
+ }
108
+ };
109
+
110
+ async function importReceiptFile(args) {
111
+ const filePath = expandPath(args?.filePath);
112
+ if (!filePath) return { error: 'filePath is required.' };
113
+
114
+ let stat;
115
+ try {
116
+ stat = statSync(filePath);
117
+ } catch {
118
+ return { error: `No file found at ${filePath}.` };
119
+ }
120
+ if (!stat.isFile()) return { error: `${filePath} is not a file.` };
121
+ if (stat.size > RECEIPT_MAX_BYTES) {
122
+ return {
123
+ error: `That file is larger than the 3 MB limit (${Math.round(stat.size / 1024)} KB).`
124
+ };
125
+ }
126
+
127
+ const mimeType = mimeFromExtension(filePath);
128
+ if (!mimeType) {
129
+ return { error: 'Unsupported file type. Use a PDF, JPEG, PNG, WebP, GIF, or HEIC receipt.' };
130
+ }
131
+
132
+ const data = await readFile(filePath);
133
+ return call('tools/call', {
134
+ name: 'propose_expense_from_receipt',
135
+ arguments: { filename: basename(filePath), mimeType, dataBase64: data.toString('base64') }
136
+ });
137
+ }
138
+
53
139
  const server = new Server(
54
- { name: 'frgur-books', version: '0.1.0' },
140
+ { name: 'frgur-books', version: PKG.version },
55
141
  { capabilities: { tools: {} } }
56
142
  );
57
143
 
58
144
  // Tool list comes straight from frugr, so adding a tool server-side surfaces it
59
- // to every client with no package update.
145
+ // to every client with no package update. The local file bridge is appended
146
+ // only when the server actually offers receipt import (keeps old servers safe).
60
147
  server.setRequestHandler(ListToolsRequestSchema, async () => {
61
148
  const { tools } = await call('tools/list');
62
- return { tools: tools ?? [] };
149
+ const list = tools ?? [];
150
+ const hasReceiptImport = list.some((t) => t.name === 'propose_expense_from_receipt');
151
+ return { tools: hasReceiptImport ? [...list, IMPORT_RECEIPT_TOOL] : list };
63
152
  });
64
153
 
65
154
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
66
155
  const { name, arguments: args } = request.params;
67
- const result = await call('tools/call', { name, arguments: args ?? {} });
156
+ // Intercept the client-side file tool before forwarding.
157
+ const result =
158
+ name === 'import_receipt_file'
159
+ ? await importReceiptFile(args ?? {})
160
+ : await call('tools/call', { name, arguments: args ?? {} });
68
161
  // frugr already returns an MCP CallTool result ({ content, isError }); pass it
69
162
  // through, falling back to stringifying anything unexpected.
70
163
  return {
@@ -74,4 +167,4 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
74
167
  });
75
168
 
76
169
  await server.connect(new StdioServerTransport());
77
- logErr(`connected to ${API_URL} — read-only books tools available.`);
170
+ logErr(`connected to ${API_URL} — books tools available (reads + review-queue proposals).`);
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
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).",
3
+ "version": "0.3.0",
4
+ "description": "MCP bridge to your frugr books — read tools (P&L, transactions, invoices, bills, reconciliation, time/mileage, snapshots) plus review-queue proposals incl. importing a receipt from a local file. Bring your own AI client.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "frgur-mcp": "index.mjs"
8
8
  },
9
9
  "files": [
10
10
  "index.mjs",
11
- "README.md"
11
+ "README.md",
12
+ "CHANGELOG.md"
12
13
  ],
14
+ "scripts": {
15
+ "sync-frugr-alias": "node scripts/sync-frugr-alias.mjs",
16
+ "prepublishOnly": "npm run sync-frugr-alias"
17
+ },
13
18
  "engines": {
14
19
  "node": ">=18"
15
20
  },