@hoagsmedia/frgur-mcp-server 0.2.0 → 0.3.1
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.
- package/CHANGELOG.md +10 -0
- package/README.md +7 -7
- package/index.mjs +124 -11
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-07-06
|
|
4
|
+
|
|
5
|
+
- **Skip the double AI pass (HOA-423).** `import_receipt_file` now takes an optional `extracted` object (`{vendor, amountCents, date, category?}`) — the fields your AI client already read off the receipt. When provided, frugr uses them directly instead of running (and billing) its own server-side vision extraction. The tool description instructs the model to include it whenever it has seen the receipt. Fields still land in Needs Review for human approval either way.
|
|
6
|
+
|
|
7
|
+
## 0.3.0 — 2026-07-06
|
|
8
|
+
|
|
9
|
+
- **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."_
|
|
10
|
+
- The local tool is advertised only when the connected frugr server actually offers receipt import, so the bridge stays compatible with older deployments.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
3
13
|
## 0.2.0 — 2026-07-02
|
|
4
14
|
|
|
5
15
|
- Document all **12** read-only books tools (was marketed as 3 in the npm description).
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
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
4
|
|
|
5
5
|
A thin, **read-only** [MCP](https://modelcontextprotocol.io) bridge to your
|
|
6
|
-
[frugr](https://
|
|
6
|
+
[frugr](https://frugr.app) books. Point your own AI client — Cursor, Claude
|
|
7
7
|
Desktop, Claude Code, or any MCP-capable app — at your accounting data and ask
|
|
8
8
|
questions in plain language.
|
|
9
9
|
|
|
@@ -45,7 +45,7 @@ at any time.
|
|
|
45
45
|
"command": "npx",
|
|
46
46
|
"args": ["-y", "@hoagsmedia/frugr-mcp@0.2.0"],
|
|
47
47
|
"env": {
|
|
48
|
-
"FRGUR_API_URL": "https://app
|
|
48
|
+
"FRGUR_API_URL": "https://frugr.app",
|
|
49
49
|
"FRGUR_API_TOKEN": "frgur_pat_your_token_here"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -63,15 +63,15 @@ Prefer the **`frugr`** spelling? Use `@hoagsmedia/frugr-mcp@0.2.0` instead — i
|
|
|
63
63
|
|
|
64
64
|
## Environment
|
|
65
65
|
|
|
66
|
-
| Variable | Required | Default
|
|
67
|
-
| ----------------- | -------- |
|
|
68
|
-
| `FRGUR_API_TOKEN` | yes | —
|
|
69
|
-
| `FRGUR_API_URL` | no | `https://app
|
|
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 |
|
|
70
70
|
|
|
71
71
|
## Verifying a token
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
|
-
curl -H "Authorization: Bearer frgur_pat_…" https://app
|
|
74
|
+
curl -H "Authorization: Bearer frgur_pat_…" https://frugr.app/api/mcp
|
|
75
75
|
# → {"ok":true,"transport":"frgur-mcp","tools":["get_profit_loss", ...]}
|
|
76
76
|
```
|
|
77
77
|
|
package/index.mjs
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @hoagsmedia/frgur-mcp-server — a thin
|
|
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
|
|
7
|
-
// own AI client's model (not frugr's)
|
|
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
|
|
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';
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
16
|
-
import {
|
|
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';
|
|
17
22
|
import { fileURLToPath } from 'node:url';
|
|
18
23
|
|
|
19
24
|
const PKG = JSON.parse(
|
|
20
25
|
readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
|
|
21
26
|
);
|
|
22
27
|
|
|
23
|
-
const API_URL = (process.env.FRGUR_API_URL ?? 'https://app
|
|
28
|
+
const API_URL = (process.env.FRGUR_API_URL ?? 'https://frugr.app').replace(/\/+$/, '');
|
|
24
29
|
const API_TOKEN = process.env.FRGUR_API_TOKEN ?? '';
|
|
25
30
|
const ENDPOINT = `${API_URL}/api/mcp`;
|
|
26
31
|
|
|
@@ -57,21 +62,129 @@ async function call(method, params) {
|
|
|
57
62
|
return data;
|
|
58
63
|
}
|
|
59
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 the machine running THIS MCP bridge process (e.g. ' +
|
|
96
|
+
"~/Downloads/receipt.pdf on the user's computer). filePath resolves against the bridge " +
|
|
97
|
+
"host's filesystem ONLY — paths from a chat-upload sandbox (e.g. /mnt/user-data/uploads/…) " +
|
|
98
|
+
'or any other machine will NEVER resolve here; do not retry them. For files you can read ' +
|
|
99
|
+
'but the bridge cannot, call propose_expense_from_receipt with base64 instead (3 MB decoded ' +
|
|
100
|
+
'cap; downscale large scans first). Max 3 MB; PDF, JPEG, PNG, WebP, GIF, or HEIC. If you ' +
|
|
101
|
+
'have already seen the receipt contents, ALWAYS include `extracted` — frugr then uses your ' +
|
|
102
|
+
'reading instead of running (and billing) its own AI extraction.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
filePath: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Absolute or ~-relative path to the receipt file on this machine.'
|
|
109
|
+
},
|
|
110
|
+
extracted: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
description:
|
|
113
|
+
'The fields you already read off this receipt. Preferred whenever you have seen it.',
|
|
114
|
+
properties: {
|
|
115
|
+
vendor: { type: 'string', description: 'Merchant name as printed.' },
|
|
116
|
+
amountCents: {
|
|
117
|
+
type: 'integer',
|
|
118
|
+
description: 'Receipt TOTAL in cents, positive (e.g. $31.03 -> 3103).'
|
|
119
|
+
},
|
|
120
|
+
date: { type: 'string', description: 'Receipt date, YYYY-MM-DD.' },
|
|
121
|
+
category: { type: 'string', description: 'Optional expense category suggestion.' }
|
|
122
|
+
},
|
|
123
|
+
required: ['amountCents']
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
required: ['filePath']
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
async function importReceiptFile(args) {
|
|
131
|
+
const filePath = expandPath(args?.filePath);
|
|
132
|
+
if (!filePath) return { error: 'filePath is required.' };
|
|
133
|
+
|
|
134
|
+
let stat;
|
|
135
|
+
try {
|
|
136
|
+
stat = statSync(filePath);
|
|
137
|
+
} catch {
|
|
138
|
+
return { error: `No file found at ${filePath}.` };
|
|
139
|
+
}
|
|
140
|
+
if (!stat.isFile()) return { error: `${filePath} is not a file.` };
|
|
141
|
+
if (stat.size > RECEIPT_MAX_BYTES) {
|
|
142
|
+
return {
|
|
143
|
+
error: `That file is larger than the 3 MB limit (${Math.round(stat.size / 1024)} KB).`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const mimeType = mimeFromExtension(filePath);
|
|
148
|
+
if (!mimeType) {
|
|
149
|
+
return { error: 'Unsupported file type. Use a PDF, JPEG, PNG, WebP, GIF, or HEIC receipt.' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const data = await readFile(filePath);
|
|
153
|
+
return call('tools/call', {
|
|
154
|
+
name: 'propose_expense_from_receipt',
|
|
155
|
+
arguments: {
|
|
156
|
+
filename: basename(filePath),
|
|
157
|
+
mimeType,
|
|
158
|
+
dataBase64: data.toString('base64'),
|
|
159
|
+
...(args?.extracted && typeof args.extracted === 'object'
|
|
160
|
+
? { extracted: args.extracted }
|
|
161
|
+
: {})
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
60
166
|
const server = new Server(
|
|
61
167
|
{ name: 'frgur-books', version: PKG.version },
|
|
62
168
|
{ capabilities: { tools: {} } }
|
|
63
169
|
);
|
|
64
170
|
|
|
65
171
|
// Tool list comes straight from frugr, so adding a tool server-side surfaces it
|
|
66
|
-
// to every client with no package update.
|
|
172
|
+
// to every client with no package update. The local file bridge is appended
|
|
173
|
+
// only when the server actually offers receipt import (keeps old servers safe).
|
|
67
174
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
68
175
|
const { tools } = await call('tools/list');
|
|
69
|
-
|
|
176
|
+
const list = tools ?? [];
|
|
177
|
+
const hasReceiptImport = list.some((t) => t.name === 'propose_expense_from_receipt');
|
|
178
|
+
return { tools: hasReceiptImport ? [...list, IMPORT_RECEIPT_TOOL] : list };
|
|
70
179
|
});
|
|
71
180
|
|
|
72
181
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
73
182
|
const { name, arguments: args } = request.params;
|
|
74
|
-
|
|
183
|
+
// Intercept the client-side file tool before forwarding.
|
|
184
|
+
const result =
|
|
185
|
+
name === 'import_receipt_file'
|
|
186
|
+
? await importReceiptFile(args ?? {})
|
|
187
|
+
: await call('tools/call', { name, arguments: args ?? {} });
|
|
75
188
|
// frugr already returns an MCP CallTool result ({ content, isError }); pass it
|
|
76
189
|
// through, falling back to stringifying anything unexpected.
|
|
77
190
|
return {
|
|
@@ -81,4 +194,4 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
81
194
|
});
|
|
82
195
|
|
|
83
196
|
await server.connect(new StdioServerTransport());
|
|
84
|
-
logErr(`connected to ${API_URL} —
|
|
197
|
+
logErr(`connected to ${API_URL} — books tools available (reads + review-queue proposals).`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoagsmedia/frgur-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
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"
|