ledger-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Novadyne (an Infai company)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: ledger-mcp
3
+ Version: 0.1.0
4
+ Summary: Double-entry accounting MCP server for AI agents — create accounts, post balanced journal entries, pull trial-balance / general-ledger reports. Paid per call via x402 micropayments (USDC on Base).
5
+ Project-URL: Homepage, https://ledger.novadyne.ai
6
+ Project-URL: Repository, https://github.com/novadyne-hq/ledger-mcp
7
+ Project-URL: Issues, https://github.com/novadyne-hq/ledger-mcp/issues
8
+ Author-email: Novadyne <support@novadyne.ai>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: accounting,agent-payments,base,bookkeeping,claude-code,double-entry,finance,ledger,mcp,mcp-server,micropayments,usdc,x402
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: eth-account>=0.11
26
+ Requires-Dist: mcp>=1.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Ledger MCP
30
+
31
+ **Double-entry accounting for AI agents.** An MCP server over the
32
+ [Ledger API](https://ledger.novadyne.ai) — create a chart of accounts, post
33
+ balanced journal entries, and pull trial-balance / general-ledger reports,
34
+ all from inside your agent.
35
+
36
+ Ledger is **agent-native**: it's paid **per call via [x402](https://x402.org)
37
+ micropayments** (USDC on the Base network), so an agent can use real
38
+ double-entry bookkeeping without an account, API key, or subscription — it just
39
+ pays a fraction of a cent per call from its own wallet.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ uvx ledger-mcp
45
+ ```
46
+
47
+ Or add it to your MCP client (`claude-code-config.json` / Claude Desktop):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "ledger": {
53
+ "command": "uvx",
54
+ "args": ["ledger-mcp"],
55
+ "env": {
56
+ "LEDGER_X402_PRIVATE_KEY": "0xYOUR_FUNDED_BASE_WALLET_KEY"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Payment
64
+
65
+ Most tools are **paid per call** (reads ~$0.002, writes ~$0.01, in USDC on Base):
66
+
67
+ - Set `LEDGER_X402_PRIVATE_KEY` to the private key of a **funded Base wallet**
68
+ (holding USDC). The server signs the x402 (EIP-3009) payment automatically on
69
+ each call — gasless, you only spend the USDC the call costs.
70
+ - Without a key, `health` and `discover` still work, and paid tools return the
71
+ exact price plus how to enable payment (nothing is spent).
72
+ - Use a **dedicated low-balance wallet** for your agent — never your main key.
73
+ The balance is the blast radius.
74
+
75
+ Run `discover` to see every endpoint and its current price before spending.
76
+
77
+ ## Tools
78
+
79
+ | Tool | Cost | What it does |
80
+ |------|------|--------------|
81
+ | `health` | free | API status + version |
82
+ | `discover` | free | List paid endpoints + x402 prices |
83
+ | `create_account` | write | Add an account (asset/liability/equity/revenue/expense) |
84
+ | `list_accounts` | read | Chart of accounts + balances |
85
+ | `get_account` | read | One account's balance (optionally as-of a date) |
86
+ | `post_transaction` | write | Record a balanced journal entry |
87
+ | `list_transactions` | read | Journal, newest first |
88
+ | `get_transaction` | read | One transaction with all entries |
89
+ | `reverse_transaction` | write | Post a reversing entry |
90
+ | `trial_balance` | read | All balances + debit/credit totals |
91
+ | `general_ledger` | read | Per-account detail over a date range |
92
+
93
+ ## The one rule: entries sum to zero
94
+
95
+ Every transaction is two or more entries whose **signed amounts sum to exactly
96
+ 0**. Amounts are integers in **minor units (cents)**: a **debit is positive**, a
97
+ **credit is negative**.
98
+
99
+ Record a $500 cash sale (debit Cash, credit Sales Revenue):
100
+
101
+ ```json
102
+ {
103
+ "date": "2026-06-19",
104
+ "description": "Cash sale",
105
+ "entries": [
106
+ {"account_id": 1, "amount": 50000, "memo": "cash in"},
107
+ {"account_id": 2, "amount": -50000, "memo": "sales revenue"}
108
+ ]
109
+ }
110
+ ```
111
+
112
+ The server rejects an unbalanced transaction before spending.
113
+
114
+ ## Links
115
+
116
+ - Docs & API: <https://ledger.novadyne.ai>
117
+ - API base: `https://ledger-api.novadyne.ai`
118
+ - x402 discovery: `https://ledger-api.novadyne.ai/.well-known/x402`
119
+
120
+ MIT licensed. By [Novadyne](https://novadyne.ai). The API backend is operated by
121
+ Novadyne; this package is the open client that talks to it.
@@ -0,0 +1,93 @@
1
+ # Ledger MCP
2
+
3
+ **Double-entry accounting for AI agents.** An MCP server over the
4
+ [Ledger API](https://ledger.novadyne.ai) — create a chart of accounts, post
5
+ balanced journal entries, and pull trial-balance / general-ledger reports,
6
+ all from inside your agent.
7
+
8
+ Ledger is **agent-native**: it's paid **per call via [x402](https://x402.org)
9
+ micropayments** (USDC on the Base network), so an agent can use real
10
+ double-entry bookkeeping without an account, API key, or subscription — it just
11
+ pays a fraction of a cent per call from its own wallet.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ uvx ledger-mcp
17
+ ```
18
+
19
+ Or add it to your MCP client (`claude-code-config.json` / Claude Desktop):
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "ledger": {
25
+ "command": "uvx",
26
+ "args": ["ledger-mcp"],
27
+ "env": {
28
+ "LEDGER_X402_PRIVATE_KEY": "0xYOUR_FUNDED_BASE_WALLET_KEY"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Payment
36
+
37
+ Most tools are **paid per call** (reads ~$0.002, writes ~$0.01, in USDC on Base):
38
+
39
+ - Set `LEDGER_X402_PRIVATE_KEY` to the private key of a **funded Base wallet**
40
+ (holding USDC). The server signs the x402 (EIP-3009) payment automatically on
41
+ each call — gasless, you only spend the USDC the call costs.
42
+ - Without a key, `health` and `discover` still work, and paid tools return the
43
+ exact price plus how to enable payment (nothing is spent).
44
+ - Use a **dedicated low-balance wallet** for your agent — never your main key.
45
+ The balance is the blast radius.
46
+
47
+ Run `discover` to see every endpoint and its current price before spending.
48
+
49
+ ## Tools
50
+
51
+ | Tool | Cost | What it does |
52
+ |------|------|--------------|
53
+ | `health` | free | API status + version |
54
+ | `discover` | free | List paid endpoints + x402 prices |
55
+ | `create_account` | write | Add an account (asset/liability/equity/revenue/expense) |
56
+ | `list_accounts` | read | Chart of accounts + balances |
57
+ | `get_account` | read | One account's balance (optionally as-of a date) |
58
+ | `post_transaction` | write | Record a balanced journal entry |
59
+ | `list_transactions` | read | Journal, newest first |
60
+ | `get_transaction` | read | One transaction with all entries |
61
+ | `reverse_transaction` | write | Post a reversing entry |
62
+ | `trial_balance` | read | All balances + debit/credit totals |
63
+ | `general_ledger` | read | Per-account detail over a date range |
64
+
65
+ ## The one rule: entries sum to zero
66
+
67
+ Every transaction is two or more entries whose **signed amounts sum to exactly
68
+ 0**. Amounts are integers in **minor units (cents)**: a **debit is positive**, a
69
+ **credit is negative**.
70
+
71
+ Record a $500 cash sale (debit Cash, credit Sales Revenue):
72
+
73
+ ```json
74
+ {
75
+ "date": "2026-06-19",
76
+ "description": "Cash sale",
77
+ "entries": [
78
+ {"account_id": 1, "amount": 50000, "memo": "cash in"},
79
+ {"account_id": 2, "amount": -50000, "memo": "sales revenue"}
80
+ ]
81
+ }
82
+ ```
83
+
84
+ The server rejects an unbalanced transaction before spending.
85
+
86
+ ## Links
87
+
88
+ - Docs & API: <https://ledger.novadyne.ai>
89
+ - API base: `https://ledger-api.novadyne.ai`
90
+ - x402 discovery: `https://ledger-api.novadyne.ai/.well-known/x402`
91
+
92
+ MIT licensed. By [Novadyne](https://novadyne.ai). The API backend is operated by
93
+ Novadyne; this package is the open client that talks to it.
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "ledger": {
4
+ "command": "uvx",
5
+ "args": ["ledger-mcp"],
6
+ "env": {
7
+ "LEDGER_X402_PRIVATE_KEY": "0xYOUR_FUNDED_BASE_WALLET_KEY"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,5 @@
1
+ """Ledger MCP — double-entry accounting for AI agents, paid per call via x402."""
2
+ from ledger_mcp.server import main
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["main"]
@@ -0,0 +1,3 @@
1
+ from ledger_mcp.server import main
2
+
3
+ main()
@@ -0,0 +1,371 @@
1
+ """Ledger MCP Server — double-entry accounting for AI agents.
2
+
3
+ Exposes the Ledger API (https://ledger-api.novadyne.ai) as MCP tools:
4
+ - create_account: add an account to the chart of accounts
5
+ - list_accounts / get_account: read the chart of accounts + balances
6
+ - post_transaction: record a balanced journal entry (entries sum to zero)
7
+ - list_transactions / get_transaction: read the journal
8
+ - reverse_transaction: post a reversing entry
9
+ - trial_balance: every account balance + debit/credit totals as of a date
10
+ - general_ledger: per-account transaction history over a date range
11
+
12
+ The Ledger API is **paid per call via x402 micropayments** (USDC on Base).
13
+ This server pays automatically when LEDGER_X402_PRIVATE_KEY is set to a
14
+ funded Base wallet; without it, the read/write tools return the price and
15
+ how to enable payment (health, schema discovery, and pricing work keyless).
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import secrets
21
+ import time
22
+ import urllib.parse
23
+ import urllib.request
24
+ import urllib.error
25
+
26
+ from mcp.server import FastMCP
27
+
28
+ DEFAULT_API_URL = "https://ledger-api.novadyne.ai"
29
+ API_URL = os.environ.get("LEDGER_API_URL", DEFAULT_API_URL).rstrip("/")
30
+
31
+ # A funded Base wallet private key (hex, with or without 0x). When set, the
32
+ # server signs x402 EIP-3009 payments automatically so paid tools just work.
33
+ PRIVATE_KEY = (
34
+ os.environ.get("LEDGER_X402_PRIVATE_KEY")
35
+ or os.environ.get("X402_PRIVATE_KEY")
36
+ or ""
37
+ ).strip()
38
+
39
+ # Native USDC on Base — used only as a sanity check against the 402 challenge.
40
+ USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
41
+
42
+ mcp = FastMCP(
43
+ "Ledger",
44
+ instructions=(
45
+ "Ledger is a double-entry accounting API for AI agents. "
46
+ "Create accounts, post balanced journal entries (every transaction's "
47
+ "entries must sum to zero), and pull trial-balance / general-ledger "
48
+ "reports. Amounts are signed integers in minor units (cents): a debit "
49
+ "is positive, a credit is negative, and the entries of one transaction "
50
+ "must sum to exactly 0. The API is paid per call via x402 micropayments "
51
+ "(USDC on Base) — set LEDGER_X402_PRIVATE_KEY to a funded Base wallet to "
52
+ "pay automatically. Start with trial_balance or list_accounts to see the "
53
+ "current books."
54
+ ),
55
+ )
56
+
57
+
58
+ # --- x402 payment ---
59
+
60
+ class PaymentError(Exception):
61
+ pass
62
+
63
+
64
+ def _eip3009_header(accept: dict) -> str:
65
+ """Build a base64 X-PAYMENT header (x402 v2) by signing an EIP-3009
66
+ TransferWithAuthorization for the amount the 402 challenge asks for."""
67
+ try:
68
+ from eth_account import Account
69
+ except ImportError:
70
+ raise PaymentError(
71
+ "eth-account is required to pay. Reinstall with `uvx ledger-mcp` "
72
+ "(it is a declared dependency) or `pip install eth-account`."
73
+ )
74
+
75
+ if not PRIVATE_KEY:
76
+ raise PaymentError("no wallet configured")
77
+
78
+ asset = accept.get("asset", "")
79
+ if asset and asset.lower() != USDC_BASE.lower():
80
+ raise PaymentError(f"unexpected asset {asset}; expected USDC on Base")
81
+
82
+ acct = Account.from_key(PRIVATE_KEY)
83
+ value = str(accept.get("maxAmountRequired") or accept.get("amount") or "0")
84
+ pay_to = accept["payTo"]
85
+ timeout = int(accept.get("maxTimeoutSeconds", 60))
86
+ now = int(time.time())
87
+ authorization = {
88
+ "from": acct.address,
89
+ "to": pay_to,
90
+ "value": value,
91
+ "validAfter": "0",
92
+ "validBefore": str(now + max(timeout, 60)),
93
+ "nonce": "0x" + secrets.token_hex(32),
94
+ }
95
+ extra = accept.get("extra") or {}
96
+ domain = {
97
+ "name": extra.get("name", "USD Coin"),
98
+ "version": extra.get("version", "2"),
99
+ "chainId": 8453,
100
+ "verifyingContract": asset or USDC_BASE,
101
+ }
102
+ types = {
103
+ "TransferWithAuthorization": [
104
+ {"name": "from", "type": "address"},
105
+ {"name": "to", "type": "address"},
106
+ {"name": "value", "type": "uint256"},
107
+ {"name": "validAfter", "type": "uint256"},
108
+ {"name": "validBefore", "type": "uint256"},
109
+ {"name": "nonce", "type": "bytes32"},
110
+ ]
111
+ }
112
+ message = {
113
+ "from": acct.address,
114
+ "to": pay_to,
115
+ "value": int(value),
116
+ "validAfter": 0,
117
+ "validBefore": int(authorization["validBefore"]),
118
+ "nonce": bytes.fromhex(authorization["nonce"][2:]),
119
+ }
120
+ signed = Account.sign_typed_data(PRIVATE_KEY, domain, types, message)
121
+ sig = signed.signature.hex()
122
+ if not sig.startswith("0x"):
123
+ sig = "0x" + sig
124
+ payload = {
125
+ "x402Version": 2,
126
+ "scheme": accept.get("scheme", "exact"),
127
+ "network": accept.get("network", "eip155:8453"),
128
+ "payload": {
129
+ "signature": sig,
130
+ "authorization": authorization,
131
+ },
132
+ }
133
+ import base64
134
+ return base64.b64encode(json.dumps(payload).encode()).decode()
135
+
136
+
137
+ def _request(method: str, path: str, query: dict | None = None, body: dict | None = None) -> dict:
138
+ """Call the Ledger API, paying the x402 challenge automatically if a wallet
139
+ is configured. Returns {"ok": True, "data": ...} or {"ok": False, "error": ...}."""
140
+ url = f"{API_URL}{path}"
141
+ if query:
142
+ clean = {k: v for k, v in query.items() if v is not None}
143
+ if clean:
144
+ url += "?" + urllib.parse.urlencode(clean)
145
+
146
+ def _do(payment_header: str | None):
147
+ headers = {"User-Agent": "ledger-mcp/0.1", "Accept": "application/json"}
148
+ data = None
149
+ if body is not None:
150
+ headers["Content-Type"] = "application/json"
151
+ data = json.dumps(body).encode()
152
+ if payment_header:
153
+ headers["X-PAYMENT"] = payment_header
154
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
155
+ return urllib.request.urlopen(req, timeout=90)
156
+
157
+ try:
158
+ with _do(None) as resp:
159
+ return {"ok": True, "data": json.loads(resp.read() or "null")}
160
+ except urllib.error.HTTPError as e:
161
+ if e.code != 402:
162
+ detail = e.read().decode() if e.fp else ""
163
+ return {"ok": False, "error": f"HTTP {e.code}: {detail[:400]}"}
164
+ challenge_raw = e.read().decode() if e.fp else "{}"
165
+ except Exception as e:
166
+ return {"ok": False, "error": f"Ledger API unreachable: {e}"}
167
+
168
+ # 402 — pay and retry
169
+ try:
170
+ challenge = json.loads(challenge_raw)
171
+ accept = (challenge.get("accepts") or [{}])[0]
172
+ price = accept.get("amount") or accept.get("maxAmountRequired")
173
+ price_usd = f"${int(price) / 1_000_000:.4f}" if price else "?"
174
+ except Exception:
175
+ accept, price_usd = {}, "?"
176
+
177
+ if not PRIVATE_KEY:
178
+ return {
179
+ "ok": False,
180
+ "needs_payment": True,
181
+ "error": (
182
+ f"This Ledger endpoint costs {price_usd} per call (x402, USDC on Base). "
183
+ "Set LEDGER_X402_PRIVATE_KEY to a funded Base wallet private key to pay "
184
+ "automatically, or pay the x402 challenge yourself. "
185
+ "Free/keyless: health() and discover()."
186
+ ),
187
+ }
188
+
189
+ try:
190
+ header = _eip3009_header(accept)
191
+ except PaymentError as e:
192
+ return {"ok": False, "error": f"Could not build payment: {e}"}
193
+
194
+ try:
195
+ with _do(header) as resp:
196
+ return {"ok": True, "data": json.loads(resp.read() or "null"), "paid": price_usd}
197
+ except urllib.error.HTTPError as e:
198
+ detail = e.read().decode() if e.fp else ""
199
+ return {"ok": False, "error": f"Payment rejected — HTTP {e.code}: {detail[:400]}"}
200
+ except Exception as e:
201
+ return {"ok": False, "error": f"Ledger API unreachable after payment: {e}"}
202
+
203
+
204
+ def _out(result: dict) -> str:
205
+ """Render an API result as compact, agent-readable text."""
206
+ if not result.get("ok"):
207
+ return f"Error: {result.get('error', 'unknown')}"
208
+ data = result.get("data")
209
+ paid = f" (paid {result['paid']})" if result.get("paid") else ""
210
+ return f"{json.dumps(data, indent=2)}{paid}"
211
+
212
+
213
+ # --- MCP Tools ---
214
+
215
+ @mcp.tool()
216
+ def health() -> str:
217
+ """Check the Ledger API status (free, no payment). Returns service version,
218
+ whether x402 payment is enabled, and the capability-token public key."""
219
+ return _out(_request("GET", "/health"))
220
+
221
+
222
+ @mcp.tool()
223
+ def discover() -> str:
224
+ """List the paid Ledger endpoints and their x402 prices (free, no payment).
225
+ Reads the x402 discovery document — useful before spending."""
226
+ return _out(_request("GET", "/.well-known/x402"))
227
+
228
+
229
+ @mcp.tool()
230
+ def create_account(name: str, type: str, parent_id: int = 0) -> str:
231
+ """Create an account in the chart of accounts. (Paid: write.)
232
+
233
+ Args:
234
+ name: Account name, e.g. "Cash", "Sales Revenue", "Accounts Payable".
235
+ type: One of asset | liability | equity | revenue | expense.
236
+ parent_id: Optional parent account id for a sub-account (0 = top level).
237
+ """
238
+ return _out(_request("POST", "/ledger/accounts",
239
+ body={"name": name, "type": type, "parent_id": parent_id}))
240
+
241
+
242
+ @mcp.tool()
243
+ def list_accounts(include_inactive: bool = False) -> str:
244
+ """List all accounts in the chart of accounts with current balances. (Paid: read.)
245
+
246
+ Args:
247
+ include_inactive: Also include deactivated accounts.
248
+ """
249
+ return _out(_request("GET", "/ledger/accounts",
250
+ query={"include_inactive": str(include_inactive).lower()}))
251
+
252
+
253
+ @mcp.tool()
254
+ def get_account(account_id: int, as_of: str | None = None) -> str:
255
+ """Get one account and its balance, optionally as of a date. (Paid: read.)
256
+
257
+ Args:
258
+ account_id: The account id.
259
+ as_of: Balance as of this date (YYYY-MM-DD); omit for current.
260
+ """
261
+ return _out(_request("GET", f"/ledger/accounts/{account_id}", query={"as_of": as_of}))
262
+
263
+
264
+ @mcp.tool()
265
+ def post_transaction(date: str, entries: list[dict], description: str = "",
266
+ reference: str | None = None) -> str:
267
+ """Record a balanced double-entry journal transaction. (Paid: write.)
268
+
269
+ Every transaction is two or more entries whose signed amounts sum to ZERO.
270
+ Amounts are integers in MINOR units (cents): a debit is POSITIVE, a credit
271
+ is NEGATIVE. Example — record a $500 cash sale (debit Cash, credit Revenue):
272
+ entries = [
273
+ {"account_id": 1, "amount": 50000, "memo": "cash in"},
274
+ {"account_id": 2, "amount": -50000, "memo": "sales revenue"}
275
+ ]
276
+
277
+ Args:
278
+ date: Transaction date, YYYY-MM-DD.
279
+ entries: List of {"account_id": int, "amount": int (cents, +debit/-credit),
280
+ "memo": optional str}. Must have >=2 entries summing to 0.
281
+ description: Human-readable description of the transaction.
282
+ reference: Optional external reference (invoice #, etc.).
283
+ """
284
+ total = sum(int(e.get("amount", 0)) for e in entries)
285
+ if total != 0:
286
+ return (f"Error: entries must sum to zero (double-entry); they sum to {total} "
287
+ f"cents. Adjust the amounts so debits (+) and credits (-) balance.")
288
+ body = {"date": date, "description": description, "entries": entries}
289
+ if reference is not None:
290
+ body["reference"] = reference
291
+ return _out(_request("POST", "/ledger/transactions", body=body))
292
+
293
+
294
+ @mcp.tool()
295
+ def list_transactions(from_date: str | None = None, to_date: str | None = None,
296
+ limit: int = 50, cursor: int = 0) -> str:
297
+ """List journal transactions, newest first. (Paid: read.)
298
+
299
+ Args:
300
+ from_date: Only transactions on/after this date (YYYY-MM-DD).
301
+ to_date: Only transactions on/before this date (YYYY-MM-DD).
302
+ limit: Max rows (default 50).
303
+ cursor: Pagination cursor from a previous response.
304
+ """
305
+ return _out(_request("GET", "/ledger/transactions",
306
+ query={"from": from_date, "to": to_date, "limit": limit, "cursor": cursor}))
307
+
308
+
309
+ @mcp.tool()
310
+ def get_transaction(tx_id: int) -> str:
311
+ """Get a single transaction with all its entries. (Paid: read.)
312
+
313
+ Args:
314
+ tx_id: The transaction id.
315
+ """
316
+ return _out(_request("GET", f"/ledger/transactions/{tx_id}"))
317
+
318
+
319
+ @mcp.tool()
320
+ def reverse_transaction(tx_id: int, reason: str = "reversal") -> str:
321
+ """Post a reversing transaction that negates an existing one. (Paid: write.)
322
+
323
+ Args:
324
+ tx_id: The transaction id to reverse.
325
+ reason: Why it's being reversed (recorded on the reversing entry).
326
+ """
327
+ return _out(_request("POST", f"/ledger/transactions/{tx_id}/reverse",
328
+ body={"reason": reason}))
329
+
330
+
331
+ @mcp.tool()
332
+ def trial_balance(as_of: str | None = None) -> str:
333
+ """Trial balance: every account with its balance, plus total debits and
334
+ total credits (which must be equal in a balanced book). (Paid: read.)
335
+
336
+ Args:
337
+ as_of: Balances as of this date (YYYY-MM-DD); omit for current.
338
+ """
339
+ return _out(_request("GET", "/ledger/reports/trial-balance", query={"as_of": as_of}))
340
+
341
+
342
+ @mcp.tool()
343
+ def general_ledger(from_date: str, to_date: str | None = None,
344
+ account_id: int | None = None) -> str:
345
+ """General ledger: per-account transaction detail over a date range. (Paid: read.)
346
+
347
+ Args:
348
+ from_date: Start date (YYYY-MM-DD), required.
349
+ to_date: End date (YYYY-MM-DD); omit for through-today.
350
+ account_id: Restrict to one account; omit for all.
351
+ """
352
+ return _out(_request("GET", "/ledger/reports/general-ledger",
353
+ query={"from": from_date, "to": to_date, "account_id": account_id}))
354
+
355
+
356
+ def main():
357
+ import argparse
358
+ parser = argparse.ArgumentParser(description="Ledger MCP Server")
359
+ parser.add_argument("--transport", choices=["stdio", "sse"],
360
+ default=os.environ.get("LEDGER_TRANSPORT", "stdio"))
361
+ parser.add_argument("--host", default="127.0.0.1")
362
+ parser.add_argument("--port", type=int, default=8384)
363
+ args = parser.parse_args()
364
+ if args.transport == "sse":
365
+ mcp.run(transport="sse", host=args.host, port=args.port)
366
+ else:
367
+ mcp.run(transport="stdio")
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ledger-mcp"
7
+ version = "0.1.0"
8
+ description = "Double-entry accounting MCP server for AI agents — create accounts, post balanced journal entries, pull trial-balance / general-ledger reports. Paid per call via x402 micropayments (USDC on Base)."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Novadyne", email = "support@novadyne.ai" }]
13
+ keywords = ["mcp", "mcp-server", "accounting", "ledger", "double-entry", "bookkeeping", "finance", "x402", "micropayments", "agent-payments", "claude-code", "base", "usdc"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "Topic :: Office/Business :: Financial :: Accounting",
19
+ "Framework :: FastAPI",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: OS Independent",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = ["mcp>=1.0", "eth-account>=0.11"]
29
+
30
+ [project.scripts]
31
+ ledger-mcp = "ledger_mcp:main"
32
+
33
+ [project.urls]
34
+ Homepage = "https://ledger.novadyne.ai"
35
+ Repository = "https://github.com/novadyne-hq/ledger-mcp"
36
+ Issues = "https://github.com/novadyne-hq/ledger-mcp/issues"
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://registry.modelcontextprotocol.io/schemas/server.json",
3
+ "name": "io.github.novadyne-hq/ledger-mcp",
4
+ "description": "Double-entry accounting MCP server for AI agents — create accounts, post balanced journal entries, and pull trial-balance / general-ledger reports. 11 tools. Paid per call via x402 micropayments (USDC on Base); health + discovery are free.",
5
+ "repository": "https://github.com/novadyne-hq/ledger-mcp",
6
+ "version": "0.1.0",
7
+ "packages": [
8
+ {
9
+ "registryType": "pypi",
10
+ "identifier": "ledger-mcp",
11
+ "version": "0.1.0",
12
+ "transport": {
13
+ "type": "stdio"
14
+ },
15
+ "environmentVariables": [
16
+ {
17
+ "name": "LEDGER_X402_PRIVATE_KEY",
18
+ "description": "Private key of a funded Base wallet (USDC) used to pay the x402 micropayment on each call. Optional — health and discovery work without it.",
19
+ "isSecret": true,
20
+ "isRequired": false
21
+ },
22
+ {
23
+ "name": "LEDGER_API_URL",
24
+ "description": "Override the Ledger API base URL (default https://ledger-api.novadyne.ai).",
25
+ "isRequired": false
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }