copilot-money-mcp 2.0.0 → 2.0.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/README.md +3 -12
- package/dist/cli.js +170 -83
- package/dist/decode-worker.js +2 -0
- package/dist/server.js +165 -79
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,17 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org/)
|
|
7
|
-
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://github.com/ignaciohermosillacornejo/copilot-money-mcp/actions/workflows/test.yml)
|
|
9
|
+
[](https://codecov.io/gh/ignaciohermosillacornejo/copilot-money-mcp)
|
|
10
10
|
|
|
11
11
|
## Disclaimer
|
|
12
12
|
|
|
13
13
|
**This is an independent, community-driven project and is not affiliated with, endorsed by, or associated with Copilot Money or its parent company in any way.** This tool was created by an independent developer to enable AI-powered queries of locally cached data. "Copilot Money" is a trademark of its respective owner.
|
|
14
14
|
|
|
15
|
-
> [!NOTE]
|
|
16
|
-
> **Write tools are temporarily unavailable.** Copilot Money has restricted direct Firestore writes from third-party clients. The 18 write tools in this repo still exist as source, but the published CLI runs read-only. A replacement write path is being evaluated. Reads are unaffected.
|
|
17
|
-
|
|
18
15
|
## Overview
|
|
19
16
|
|
|
20
17
|
An [MCP](https://modelcontextprotocol.io/) server that gives AI assistants access to your Copilot Money personal finance data. It reads from the locally cached Firestore database (LevelDB + Protocol Buffers) on your Mac. **Reads are 100% local with zero network requests.**
|
|
@@ -161,12 +158,6 @@ Uses `get_recurring_transactions`.
|
|
|
161
158
|
| `get_cache_info` | Local cache metadata — date range, transaction count, cache age. |
|
|
162
159
|
| `refresh_database` | Reload data from disk. Cache auto-refreshes every 5 minutes. |
|
|
163
160
|
|
|
164
|
-
### Write Tools — temporarily unavailable
|
|
165
|
-
|
|
166
|
-
Copilot Money has restricted direct Firestore writes from third-party clients, so the 18 write tools (`update_transaction`, `create_budget`, `create_goal`, `update_recurring`, etc.) no longer succeed against the live backend. The code still lives in `src/` and the tool schemas are preserved for reference and future work, but the published `copilot-money-mcp` CLI runs in read-only mode. Passing `--write` prints a notice and still starts read-only.
|
|
167
|
-
|
|
168
|
-
A replacement write path (via Copilot Money's GraphQL web API) is being evaluated; progress will be tracked in the repo issues.
|
|
169
|
-
|
|
170
161
|
## Configuration
|
|
171
162
|
|
|
172
163
|
### Cache TTL
|
package/dist/cli.js
CHANGED
|
@@ -27840,6 +27840,7 @@ class StdioServerTransport {
|
|
|
27840
27840
|
}
|
|
27841
27841
|
|
|
27842
27842
|
// src/core/database.ts
|
|
27843
|
+
import { randomUUID } from "crypto";
|
|
27843
27844
|
import { existsSync, readdirSync } from "fs";
|
|
27844
27845
|
import { homedir } from "os";
|
|
27845
27846
|
import { join } from "path";
|
|
@@ -29765,6 +29766,8 @@ function processRecurring(fields, docId) {
|
|
|
29765
29766
|
}
|
|
29766
29767
|
}
|
|
29767
29768
|
function processBudget(fields, docId) {
|
|
29769
|
+
if (fields.size === 0)
|
|
29770
|
+
return null;
|
|
29768
29771
|
const budgetId = getString(fields, "budget_id") ?? docId;
|
|
29769
29772
|
const budgetData = {
|
|
29770
29773
|
budget_id: budgetId
|
|
@@ -31553,6 +31556,81 @@ class CopilotDatabase {
|
|
|
31553
31556
|
Object.assign(txn, fields);
|
|
31554
31557
|
return true;
|
|
31555
31558
|
}
|
|
31559
|
+
patchCachedBudget(categoryId, amount, month) {
|
|
31560
|
+
if (!this._budgets)
|
|
31561
|
+
return false;
|
|
31562
|
+
const monthKey = month ?? (() => {
|
|
31563
|
+
const d = new Date;
|
|
31564
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
31565
|
+
})();
|
|
31566
|
+
const existing = this._budgets.find((b) => b.category_id === categoryId);
|
|
31567
|
+
if (existing) {
|
|
31568
|
+
existing.amounts = { ...existing.amounts ?? {}, [monthKey]: amount };
|
|
31569
|
+
} else {
|
|
31570
|
+
this._budgets.push({
|
|
31571
|
+
budget_id: randomUUID(),
|
|
31572
|
+
category_id: categoryId,
|
|
31573
|
+
amounts: { [monthKey]: amount }
|
|
31574
|
+
});
|
|
31575
|
+
}
|
|
31576
|
+
return true;
|
|
31577
|
+
}
|
|
31578
|
+
patchCachedTagUpsert(tag) {
|
|
31579
|
+
if (!this._tags)
|
|
31580
|
+
return;
|
|
31581
|
+
const idx = this._tags.findIndex((t) => t.tag_id === tag.tag_id);
|
|
31582
|
+
if (idx >= 0) {
|
|
31583
|
+
Object.assign(this._tags[idx], tag);
|
|
31584
|
+
} else {
|
|
31585
|
+
this._tags.push(tag);
|
|
31586
|
+
}
|
|
31587
|
+
}
|
|
31588
|
+
patchCachedTagDelete(tagId) {
|
|
31589
|
+
if (!this._tags)
|
|
31590
|
+
return false;
|
|
31591
|
+
const before = this._tags.length;
|
|
31592
|
+
this._tags = this._tags.filter((t) => t.tag_id !== tagId);
|
|
31593
|
+
return this._tags.length < before;
|
|
31594
|
+
}
|
|
31595
|
+
patchCachedCategoryUpsert(category) {
|
|
31596
|
+
if (!this._userCategories)
|
|
31597
|
+
return;
|
|
31598
|
+
const idx = this._userCategories.findIndex((c) => c.category_id === category.category_id);
|
|
31599
|
+
if (idx >= 0) {
|
|
31600
|
+
Object.assign(this._userCategories[idx], category);
|
|
31601
|
+
} else {
|
|
31602
|
+
this._userCategories.push(category);
|
|
31603
|
+
}
|
|
31604
|
+
this._categoryNameMap = null;
|
|
31605
|
+
}
|
|
31606
|
+
patchCachedCategoryDelete(categoryId) {
|
|
31607
|
+
if (!this._userCategories)
|
|
31608
|
+
return false;
|
|
31609
|
+
const before = this._userCategories.length;
|
|
31610
|
+
this._userCategories = this._userCategories.filter((c) => c.category_id !== categoryId);
|
|
31611
|
+
if (this._userCategories.length < before) {
|
|
31612
|
+
this._categoryNameMap = null;
|
|
31613
|
+
return true;
|
|
31614
|
+
}
|
|
31615
|
+
return false;
|
|
31616
|
+
}
|
|
31617
|
+
patchCachedRecurringUpsert(recurring) {
|
|
31618
|
+
if (!this._recurring)
|
|
31619
|
+
return;
|
|
31620
|
+
const idx = this._recurring.findIndex((r) => r.recurring_id === recurring.recurring_id);
|
|
31621
|
+
if (idx >= 0) {
|
|
31622
|
+
Object.assign(this._recurring[idx], recurring);
|
|
31623
|
+
} else {
|
|
31624
|
+
this._recurring.push(recurring);
|
|
31625
|
+
}
|
|
31626
|
+
}
|
|
31627
|
+
patchCachedRecurringDelete(recurringId) {
|
|
31628
|
+
if (!this._recurring)
|
|
31629
|
+
return false;
|
|
31630
|
+
const before = this._recurring.length;
|
|
31631
|
+
this._recurring = this._recurring.filter((r) => r.recurring_id !== recurringId);
|
|
31632
|
+
return this._recurring.length < before;
|
|
31633
|
+
}
|
|
31556
31634
|
getCacheLoadedAt() {
|
|
31557
31635
|
return this._cacheLoadedAt;
|
|
31558
31636
|
}
|
|
@@ -32569,14 +32647,6 @@ var CREATE_RECURRING = `mutation CreateRecurring($input: CreateRecurringInput!)
|
|
|
32569
32647
|
createRecurring(input: $input) {
|
|
32570
32648
|
__typename
|
|
32571
32649
|
...RecurringFields
|
|
32572
|
-
rule {
|
|
32573
|
-
__typename
|
|
32574
|
-
...RecurringRuleFields
|
|
32575
|
-
}
|
|
32576
|
-
payments {
|
|
32577
|
-
__typename
|
|
32578
|
-
...RecurringPaymentFields
|
|
32579
|
-
}
|
|
32580
32650
|
}
|
|
32581
32651
|
}
|
|
32582
32652
|
|
|
@@ -32602,21 +32672,6 @@ fragment RecurringFields on Recurring {
|
|
|
32602
32672
|
state
|
|
32603
32673
|
name
|
|
32604
32674
|
id
|
|
32605
|
-
}
|
|
32606
|
-
|
|
32607
|
-
fragment RecurringRuleFields on RecurringRule {
|
|
32608
|
-
__typename
|
|
32609
|
-
nameContains
|
|
32610
|
-
minAmount
|
|
32611
|
-
maxAmount
|
|
32612
|
-
days
|
|
32613
|
-
}
|
|
32614
|
-
|
|
32615
|
-
fragment RecurringPaymentFields on RecurringPayment {
|
|
32616
|
-
__typename
|
|
32617
|
-
amount
|
|
32618
|
-
isPaid
|
|
32619
|
-
date
|
|
32620
32675
|
}`;
|
|
32621
32676
|
var EDIT_RECURRING = `mutation EditRecurring($id: ID!, $input: EditRecurringInput!) {
|
|
32622
32677
|
editRecurring(id: $id, input: $input) {
|
|
@@ -32624,14 +32679,6 @@ var EDIT_RECURRING = `mutation EditRecurring($id: ID!, $input: EditRecurringInpu
|
|
|
32624
32679
|
recurring {
|
|
32625
32680
|
__typename
|
|
32626
32681
|
...RecurringFields
|
|
32627
|
-
rule {
|
|
32628
|
-
__typename
|
|
32629
|
-
...RecurringRuleFields
|
|
32630
|
-
}
|
|
32631
|
-
payments {
|
|
32632
|
-
__typename
|
|
32633
|
-
...RecurringPaymentFields
|
|
32634
|
-
}
|
|
32635
32682
|
}
|
|
32636
32683
|
}
|
|
32637
32684
|
}
|
|
@@ -32658,21 +32705,6 @@ fragment RecurringFields on Recurring {
|
|
|
32658
32705
|
state
|
|
32659
32706
|
name
|
|
32660
32707
|
id
|
|
32661
|
-
}
|
|
32662
|
-
|
|
32663
|
-
fragment RecurringRuleFields on RecurringRule {
|
|
32664
|
-
__typename
|
|
32665
|
-
nameContains
|
|
32666
|
-
minAmount
|
|
32667
|
-
maxAmount
|
|
32668
|
-
days
|
|
32669
|
-
}
|
|
32670
|
-
|
|
32671
|
-
fragment RecurringPaymentFields on RecurringPayment {
|
|
32672
|
-
__typename
|
|
32673
|
-
amount
|
|
32674
|
-
isPaid
|
|
32675
|
-
date
|
|
32676
32708
|
}`;
|
|
32677
32709
|
var DELETE_RECURRING = `mutation DeleteRecurring($deleteRecurringId: ID!) {
|
|
32678
32710
|
deleteRecurring(id: $deleteRecurringId)
|
|
@@ -32803,20 +32835,8 @@ async function editRecurring(client, args) {
|
|
|
32803
32835
|
const changed = {};
|
|
32804
32836
|
if ("state" in args.input)
|
|
32805
32837
|
changed.state = recurring.state;
|
|
32806
|
-
if ("rule" in args.input &&
|
|
32807
|
-
|
|
32808
|
-
const stringRule = {};
|
|
32809
|
-
if ("nameContains" in serverRule)
|
|
32810
|
-
stringRule.nameContains = serverRule.nameContains;
|
|
32811
|
-
if ("minAmount" in serverRule && serverRule.minAmount !== undefined) {
|
|
32812
|
-
stringRule.minAmount = String(serverRule.minAmount);
|
|
32813
|
-
}
|
|
32814
|
-
if ("maxAmount" in serverRule && serverRule.maxAmount !== undefined) {
|
|
32815
|
-
stringRule.maxAmount = String(serverRule.maxAmount);
|
|
32816
|
-
}
|
|
32817
|
-
if ("days" in serverRule)
|
|
32818
|
-
stringRule.days = serverRule.days;
|
|
32819
|
-
changed.rule = stringRule;
|
|
32838
|
+
if ("rule" in args.input && args.input.rule) {
|
|
32839
|
+
changed.rule = { ...args.input.rule };
|
|
32820
32840
|
}
|
|
32821
32841
|
return { id: recurring.id, changed };
|
|
32822
32842
|
}
|
|
@@ -33856,23 +33876,32 @@ class CopilotMoneyTools {
|
|
|
33856
33876
|
async getBudgets(options = {}) {
|
|
33857
33877
|
const { active_only = false } = options;
|
|
33858
33878
|
const allBudgets = await this.db.getBudgets(active_only);
|
|
33879
|
+
const now = new Date;
|
|
33880
|
+
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
33881
|
+
const effectiveAmount = (b) => {
|
|
33882
|
+
const override = b.amounts?.[currentMonth];
|
|
33883
|
+
return override !== undefined ? override : b.amount;
|
|
33884
|
+
};
|
|
33885
|
+
const nonTombstone = allBudgets.filter((b) => b.category_id !== undefined || b.amount !== undefined || b.amounts !== undefined);
|
|
33859
33886
|
const categoryMap2 = await this.getUserCategoryMap();
|
|
33860
|
-
const budgets =
|
|
33887
|
+
const budgets = nonTombstone.filter((b) => {
|
|
33861
33888
|
if (!b.category_id)
|
|
33862
33889
|
return true;
|
|
33863
33890
|
return categoryMap2.has(b.category_id) || isKnownPlaidCategory(b.category_id);
|
|
33864
33891
|
});
|
|
33865
33892
|
let totalBudgeted = 0;
|
|
33866
33893
|
for (const budget of budgets) {
|
|
33867
|
-
|
|
33868
|
-
|
|
33894
|
+
const amt = effectiveAmount(budget);
|
|
33895
|
+
if (amt) {
|
|
33896
|
+
const monthlyAmount = budget.period === "yearly" ? amt / 12 : budget.period === "weekly" ? amt * 4.33 : budget.period === "daily" ? amt * 30 : amt;
|
|
33869
33897
|
totalBudgeted += monthlyAmount;
|
|
33870
33898
|
}
|
|
33871
33899
|
}
|
|
33872
33900
|
const enrichedBudgets = await Promise.all(budgets.map(async (b) => ({
|
|
33873
33901
|
budget_id: b.budget_id,
|
|
33874
33902
|
name: b.name,
|
|
33875
|
-
amount: b
|
|
33903
|
+
amount: effectiveAmount(b),
|
|
33904
|
+
...b.amounts ? { amounts: b.amounts } : {},
|
|
33876
33905
|
period: b.period,
|
|
33877
33906
|
category_id: b.category_id,
|
|
33878
33907
|
category_name: b.category_id ? await this.resolveCategoryName(b.category_id) : undefined,
|
|
@@ -34079,6 +34108,13 @@ class CopilotMoneyTools {
|
|
|
34079
34108
|
isExcluded: args.is_excluded ?? false
|
|
34080
34109
|
}
|
|
34081
34110
|
});
|
|
34111
|
+
this.db.patchCachedCategoryUpsert({
|
|
34112
|
+
category_id: result.id,
|
|
34113
|
+
name: result.name,
|
|
34114
|
+
color: result.colorName,
|
|
34115
|
+
emoji: args.emoji,
|
|
34116
|
+
excluded: args.is_excluded ?? false
|
|
34117
|
+
});
|
|
34082
34118
|
return {
|
|
34083
34119
|
success: true,
|
|
34084
34120
|
category_id: result.id,
|
|
@@ -34154,6 +34190,15 @@ class CopilotMoneyTools {
|
|
|
34154
34190
|
isReviewed: "reviewed"
|
|
34155
34191
|
};
|
|
34156
34192
|
const updated = Object.keys(result.changed).map((k) => graphqlToApiName[k] ?? k);
|
|
34193
|
+
const patch = {};
|
|
34194
|
+
if ("category_id" in args && args.category_id !== undefined)
|
|
34195
|
+
patch.category_id = args.category_id;
|
|
34196
|
+
if ("note" in args && args.note !== undefined)
|
|
34197
|
+
patch.user_note = args.note;
|
|
34198
|
+
if ("tag_ids" in args && args.tag_ids !== undefined)
|
|
34199
|
+
patch.tag_ids = args.tag_ids;
|
|
34200
|
+
if (Object.keys(patch).length > 0)
|
|
34201
|
+
this.db.patchCachedTransaction(transaction_id, patch);
|
|
34157
34202
|
return {
|
|
34158
34203
|
success: true,
|
|
34159
34204
|
transaction_id: result.id,
|
|
@@ -34224,6 +34269,9 @@ class CopilotMoneyTools {
|
|
|
34224
34269
|
}
|
|
34225
34270
|
throw error48;
|
|
34226
34271
|
}
|
|
34272
|
+
for (const id of transaction_ids) {
|
|
34273
|
+
this.db.patchCachedTransaction(id, { user_reviewed: reviewed });
|
|
34274
|
+
}
|
|
34227
34275
|
return {
|
|
34228
34276
|
success: true,
|
|
34229
34277
|
reviewed_count,
|
|
@@ -34239,6 +34287,11 @@ class CopilotMoneyTools {
|
|
|
34239
34287
|
const result = await createTag(client, {
|
|
34240
34288
|
input: { name: args.name.trim(), colorName }
|
|
34241
34289
|
});
|
|
34290
|
+
this.db.patchCachedTagUpsert({
|
|
34291
|
+
tag_id: result.id,
|
|
34292
|
+
name: result.name,
|
|
34293
|
+
color_name: result.colorName
|
|
34294
|
+
});
|
|
34242
34295
|
return {
|
|
34243
34296
|
success: true,
|
|
34244
34297
|
tag_id: result.id,
|
|
@@ -34255,6 +34308,7 @@ class CopilotMoneyTools {
|
|
|
34255
34308
|
const client = this.getGraphQLClient();
|
|
34256
34309
|
try {
|
|
34257
34310
|
const result = await deleteTag(client, { id: args.tag_id });
|
|
34311
|
+
this.db.patchCachedTagDelete(args.tag_id);
|
|
34258
34312
|
return { success: true, tag_id: result.id, deleted: true };
|
|
34259
34313
|
} catch (e) {
|
|
34260
34314
|
if (e instanceof GraphQLError)
|
|
@@ -34278,6 +34332,16 @@ class CopilotMoneyTools {
|
|
|
34278
34332
|
}
|
|
34279
34333
|
try {
|
|
34280
34334
|
const result = await editCategory(client, { id: args.category_id, input });
|
|
34335
|
+
const patch = { category_id: args.category_id };
|
|
34336
|
+
if (args.name !== undefined)
|
|
34337
|
+
patch.name = args.name;
|
|
34338
|
+
if (args.color_name !== undefined)
|
|
34339
|
+
patch.color = args.color_name;
|
|
34340
|
+
if (args.emoji !== undefined)
|
|
34341
|
+
patch.emoji = args.emoji;
|
|
34342
|
+
if (args.is_excluded !== undefined)
|
|
34343
|
+
patch.excluded = args.is_excluded;
|
|
34344
|
+
this.db.patchCachedCategoryUpsert(patch);
|
|
34281
34345
|
return {
|
|
34282
34346
|
success: true,
|
|
34283
34347
|
category_id: result.id,
|
|
@@ -34293,6 +34357,7 @@ class CopilotMoneyTools {
|
|
|
34293
34357
|
const client = this.getGraphQLClient();
|
|
34294
34358
|
try {
|
|
34295
34359
|
const result = await deleteCategory(client, { id: args.category_id });
|
|
34360
|
+
this.db.patchCachedCategoryDelete(args.category_id);
|
|
34296
34361
|
return { success: true, category_id: result.id, deleted: true };
|
|
34297
34362
|
} catch (e) {
|
|
34298
34363
|
if (e instanceof GraphQLError)
|
|
@@ -34319,6 +34384,7 @@ class CopilotMoneyTools {
|
|
|
34319
34384
|
amount: args.amount,
|
|
34320
34385
|
month: args.month
|
|
34321
34386
|
});
|
|
34387
|
+
this.db.patchCachedBudget(args.category_id, parseFloat(args.amount), args.month);
|
|
34322
34388
|
return {
|
|
34323
34389
|
success: true,
|
|
34324
34390
|
category_id: result.categoryId,
|
|
@@ -34343,6 +34409,10 @@ class CopilotMoneyTools {
|
|
|
34343
34409
|
id: args.recurring_id,
|
|
34344
34410
|
input: { state: args.state }
|
|
34345
34411
|
});
|
|
34412
|
+
this.db.patchCachedRecurringUpsert({
|
|
34413
|
+
recurring_id: args.recurring_id,
|
|
34414
|
+
state: args.state.toLowerCase()
|
|
34415
|
+
});
|
|
34346
34416
|
return { success: true, recurring_id: result.id, state: args.state };
|
|
34347
34417
|
} catch (e) {
|
|
34348
34418
|
if (e instanceof GraphQLError)
|
|
@@ -34354,6 +34424,7 @@ class CopilotMoneyTools {
|
|
|
34354
34424
|
const client = this.getGraphQLClient();
|
|
34355
34425
|
try {
|
|
34356
34426
|
const result = await deleteRecurring(client, { id: args.recurring_id });
|
|
34427
|
+
this.db.patchCachedRecurringDelete(args.recurring_id);
|
|
34357
34428
|
return { success: true, recurring_id: result.id, deleted: true };
|
|
34358
34429
|
} catch (e) {
|
|
34359
34430
|
if (e instanceof GraphQLError)
|
|
@@ -34373,6 +34444,12 @@ class CopilotMoneyTools {
|
|
|
34373
34444
|
}
|
|
34374
34445
|
try {
|
|
34375
34446
|
const result = await editTag(client, { id: args.tag_id, input });
|
|
34447
|
+
const patch = { tag_id: args.tag_id };
|
|
34448
|
+
if (args.name !== undefined)
|
|
34449
|
+
patch.name = args.name;
|
|
34450
|
+
if (args.color_name !== undefined)
|
|
34451
|
+
patch.color_name = args.color_name;
|
|
34452
|
+
this.db.patchCachedTagUpsert(patch);
|
|
34376
34453
|
return { success: true, tag_id: result.id, updated: Object.keys(result.changed) };
|
|
34377
34454
|
} catch (e) {
|
|
34378
34455
|
if (e instanceof GraphQLError)
|
|
@@ -34404,6 +34481,12 @@ class CopilotMoneyTools {
|
|
|
34404
34481
|
}
|
|
34405
34482
|
}
|
|
34406
34483
|
});
|
|
34484
|
+
this.db.patchCachedRecurringUpsert({
|
|
34485
|
+
recurring_id: result.id,
|
|
34486
|
+
name: result.name,
|
|
34487
|
+
state: result.state.toLowerCase(),
|
|
34488
|
+
frequency: result.frequency
|
|
34489
|
+
});
|
|
34407
34490
|
return {
|
|
34408
34491
|
success: true,
|
|
34409
34492
|
recurring_id: result.id,
|
|
@@ -34439,6 +34522,17 @@ class CopilotMoneyTools {
|
|
|
34439
34522
|
}
|
|
34440
34523
|
try {
|
|
34441
34524
|
const result = await editRecurring(client, { id: args.recurring_id, input });
|
|
34525
|
+
const patch = { recurring_id: args.recurring_id };
|
|
34526
|
+
if (args.state !== undefined) {
|
|
34527
|
+
patch.state = args.state.toLowerCase();
|
|
34528
|
+
}
|
|
34529
|
+
if (args.rule?.name_contains !== undefined)
|
|
34530
|
+
patch.match_string = args.rule.name_contains;
|
|
34531
|
+
if (args.rule?.min_amount !== undefined)
|
|
34532
|
+
patch.min_amount = parseFloat(args.rule.min_amount);
|
|
34533
|
+
if (args.rule?.max_amount !== undefined)
|
|
34534
|
+
patch.max_amount = parseFloat(args.rule.max_amount);
|
|
34535
|
+
this.db.patchCachedRecurringUpsert(patch);
|
|
34442
34536
|
return { success: true, recurring_id: result.id, updated: Object.keys(result.changed) };
|
|
34443
34537
|
} catch (e) {
|
|
34444
34538
|
if (e instanceof GraphQLError)
|
|
@@ -34895,7 +34989,7 @@ function createToolSchemas() {
|
|
|
34895
34989
|
},
|
|
34896
34990
|
{
|
|
34897
34991
|
name: "get_budgets",
|
|
34898
|
-
description: "Get budgets from Copilot's native budget tracking. " + "
|
|
34992
|
+
description: "Get budgets from Copilot's native budget tracking. " + "Returns the current-month effective budget per category plus the full " + "`amounts` map of per-month overrides for history lookups. For parent " + "categories, the returned `amount` is the resolved total (children + " + "rollovers) that Copilot displays in the Budgets view. Totals use the " + "current-month effective amount.",
|
|
34899
34993
|
inputSchema: {
|
|
34900
34994
|
type: "object",
|
|
34901
34995
|
properties: {
|
|
@@ -35016,7 +35110,7 @@ function createToolSchemas() {
|
|
|
35016
35110
|
},
|
|
35017
35111
|
{
|
|
35018
35112
|
name: "get_balance_history",
|
|
35019
|
-
description: "Get daily balance snapshots for accounts over time.
|
|
35113
|
+
description: "Get daily balance snapshots for accounts over time. Each entry returns current_balance, " + "available_balance, limit, account_id, and account_name. The response also includes an " + "`accounts` array listing the distinct account IDs in the paginated page. Requires a " + "granularity parameter (daily, weekly, or monthly) to control response size. Weekly and " + "monthly modes downsample by keeping the last data point per period. Filter by " + "account_id and date range.",
|
|
35020
35114
|
inputSchema: {
|
|
35021
35115
|
type: "object",
|
|
35022
35116
|
properties: {
|
|
@@ -35215,7 +35309,7 @@ function createWriteToolSchemas() {
|
|
|
35215
35309
|
},
|
|
35216
35310
|
{
|
|
35217
35311
|
name: "review_transactions",
|
|
35218
|
-
description: "Mark one or more transactions as reviewed (or unreviewed). " + "Accepts an array of transaction_ids. Writes
|
|
35312
|
+
description: "Mark one or more transactions as reviewed (or unreviewed). " + "Accepts an array of transaction_ids. Writes are issued via GraphQL in parallel with " + "a cap of 5 in flight at a time. On the first GraphQL error, new writes stop, in-flight " + "writes settle, and the error is thrown with a `reviewed_count` reflecting how many " + "succeeded before the failure (partial success is possible).",
|
|
35219
35313
|
inputSchema: {
|
|
35220
35314
|
type: "object",
|
|
35221
35315
|
properties: {
|
|
@@ -35249,11 +35343,7 @@ function createWriteToolSchemas() {
|
|
|
35249
35343
|
},
|
|
35250
35344
|
color_name: {
|
|
35251
35345
|
type: "string",
|
|
35252
|
-
description: 'Optional
|
|
35253
|
-
},
|
|
35254
|
-
hex_color: {
|
|
35255
|
-
type: "string",
|
|
35256
|
-
description: 'Optional hex color code (e.g. "#FF5733")'
|
|
35346
|
+
description: 'Optional palette token from Copilot (e.g. "PURPLE2", "OLIVE1", "RED1"). ' + 'Defaults to "PURPLE2" when omitted. See existing tags for valid values.'
|
|
35257
35347
|
}
|
|
35258
35348
|
},
|
|
35259
35349
|
required: ["name"]
|
|
@@ -35373,7 +35463,7 @@ function createWriteToolSchemas() {
|
|
|
35373
35463
|
},
|
|
35374
35464
|
{
|
|
35375
35465
|
name: "set_budget",
|
|
35376
|
-
description: 'Set the monthly budget amount for a category. amount="0" clears the budget. ' + 'Pass month="YYYY-MM" for a single-month override; omit for the all-months default. ' + 'Note: if the user has disabled "Enable budgeting" or "Enable rollover" in ' + "Copilot → Settings → General, the budget write still succeeds on the server, but " + "the value will not appear in the Copilot UI until those toggles are re-enabled. " + 'Rollover behavior also depends on the "Rollover categories" selection in the same ' + "settings pane, which is not writable through this tool.
|
|
35466
|
+
description: 'Set the monthly budget amount for a category. amount="0" clears the budget. ' + 'Pass month="YYYY-MM" for a single-month override; omit for the all-months default. ' + 'Note: if the user has disabled "Enable budgeting" or "Enable rollover" in ' + "Copilot → Settings → General, the budget write still succeeds on the server, but " + "the value will not appear in the Copilot UI until those toggles are re-enabled. " + 'Rollover behavior also depends on the "Rollover categories" selection in the same ' + "settings pane, which is not writable through this tool.",
|
|
35377
35467
|
inputSchema: {
|
|
35378
35468
|
type: "object",
|
|
35379
35469
|
properties: {
|
|
@@ -35444,7 +35534,7 @@ function createWriteToolSchemas() {
|
|
|
35444
35534
|
},
|
|
35445
35535
|
{
|
|
35446
35536
|
name: "update_tag",
|
|
35447
|
-
description: "Update an existing tag. Provide tag_id (required) and at least one of name
|
|
35537
|
+
description: "Update an existing tag. Provide tag_id (required) and at least one of name or " + "color_name. Only the specified fields are updated. " + "Writes directly to Copilot Money via GraphQL.",
|
|
35448
35538
|
inputSchema: {
|
|
35449
35539
|
type: "object",
|
|
35450
35540
|
properties: {
|
|
@@ -35458,11 +35548,7 @@ function createWriteToolSchemas() {
|
|
|
35458
35548
|
},
|
|
35459
35549
|
color_name: {
|
|
35460
35550
|
type: "string",
|
|
35461
|
-
description: 'New
|
|
35462
|
-
},
|
|
35463
|
-
hex_color: {
|
|
35464
|
-
type: "string",
|
|
35465
|
-
description: 'New hex color code (e.g. "#FF5733")'
|
|
35551
|
+
description: 'New palette token from Copilot (e.g. "PURPLE2", "OLIVE1", "RED1"). ' + "See existing tags for valid values."
|
|
35466
35552
|
}
|
|
35467
35553
|
},
|
|
35468
35554
|
required: ["tag_id"]
|
|
@@ -36009,6 +36095,7 @@ Usage:
|
|
|
36009
36095
|
Options:
|
|
36010
36096
|
--db-path <path> Path to LevelDB database (default: Copilot Money's default location)
|
|
36011
36097
|
--timeout <ms> Decode timeout in milliseconds (default: 90000 = 90 seconds)
|
|
36098
|
+
--write Enable write tools (sends authenticated requests to Copilot Money's GraphQL API)
|
|
36012
36099
|
--verbose, -v Enable verbose logging
|
|
36013
36100
|
--help, -h Show this help message
|
|
36014
36101
|
|
|
@@ -36038,9 +36125,6 @@ function configureLogging(verbose) {
|
|
|
36038
36125
|
async function main() {
|
|
36039
36126
|
const { dbPath, verbose, timeoutMs, writeFlagSeen } = parseArgs();
|
|
36040
36127
|
configureLogging(verbose);
|
|
36041
|
-
if (writeFlagSeen) {
|
|
36042
|
-
console.error("[copilot-money-mcp] --write is temporarily unavailable: Copilot Money " + "has restricted direct Firestore writes from third-party clients. " + "Starting in read-only mode. Status: " + "https://github.com/ignaciohermosillacornejo/copilot-money-mcp/issues");
|
|
36043
|
-
}
|
|
36044
36128
|
try {
|
|
36045
36129
|
if (verbose) {
|
|
36046
36130
|
console.log("Starting Copilot Money MCP Server...");
|
|
@@ -36049,8 +36133,11 @@ async function main() {
|
|
|
36049
36133
|
} else {
|
|
36050
36134
|
console.log("Using default Copilot Money database location");
|
|
36051
36135
|
}
|
|
36136
|
+
if (writeFlagSeen) {
|
|
36137
|
+
console.log("Write tools enabled (--write)");
|
|
36138
|
+
}
|
|
36052
36139
|
}
|
|
36053
|
-
await runServer(dbPath, timeoutMs,
|
|
36140
|
+
await runServer(dbPath, timeoutMs, writeFlagSeen);
|
|
36054
36141
|
} catch (error48) {
|
|
36055
36142
|
console.error("Server error:", error48);
|
|
36056
36143
|
process.exit(1);
|
package/dist/decode-worker.js
CHANGED
|
@@ -15171,6 +15171,8 @@ function processRecurring(fields, docId) {
|
|
|
15171
15171
|
}
|
|
15172
15172
|
}
|
|
15173
15173
|
function processBudget(fields, docId) {
|
|
15174
|
+
if (fields.size === 0)
|
|
15175
|
+
return null;
|
|
15174
15176
|
const budgetId = getString(fields, "budget_id") ?? docId;
|
|
15175
15177
|
const budgetData = {
|
|
15176
15178
|
budget_id: budgetId
|
package/dist/server.js
CHANGED
|
@@ -27839,6 +27839,7 @@ class StdioServerTransport {
|
|
|
27839
27839
|
}
|
|
27840
27840
|
|
|
27841
27841
|
// src/core/database.ts
|
|
27842
|
+
import { randomUUID } from "crypto";
|
|
27842
27843
|
import { existsSync, readdirSync } from "fs";
|
|
27843
27844
|
import { homedir } from "os";
|
|
27844
27845
|
import { join } from "path";
|
|
@@ -29764,6 +29765,8 @@ function processRecurring(fields, docId) {
|
|
|
29764
29765
|
}
|
|
29765
29766
|
}
|
|
29766
29767
|
function processBudget(fields, docId) {
|
|
29768
|
+
if (fields.size === 0)
|
|
29769
|
+
return null;
|
|
29767
29770
|
const budgetId = getString(fields, "budget_id") ?? docId;
|
|
29768
29771
|
const budgetData = {
|
|
29769
29772
|
budget_id: budgetId
|
|
@@ -31552,6 +31555,81 @@ class CopilotDatabase {
|
|
|
31552
31555
|
Object.assign(txn, fields);
|
|
31553
31556
|
return true;
|
|
31554
31557
|
}
|
|
31558
|
+
patchCachedBudget(categoryId, amount, month) {
|
|
31559
|
+
if (!this._budgets)
|
|
31560
|
+
return false;
|
|
31561
|
+
const monthKey = month ?? (() => {
|
|
31562
|
+
const d = new Date;
|
|
31563
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
31564
|
+
})();
|
|
31565
|
+
const existing = this._budgets.find((b) => b.category_id === categoryId);
|
|
31566
|
+
if (existing) {
|
|
31567
|
+
existing.amounts = { ...existing.amounts ?? {}, [monthKey]: amount };
|
|
31568
|
+
} else {
|
|
31569
|
+
this._budgets.push({
|
|
31570
|
+
budget_id: randomUUID(),
|
|
31571
|
+
category_id: categoryId,
|
|
31572
|
+
amounts: { [monthKey]: amount }
|
|
31573
|
+
});
|
|
31574
|
+
}
|
|
31575
|
+
return true;
|
|
31576
|
+
}
|
|
31577
|
+
patchCachedTagUpsert(tag) {
|
|
31578
|
+
if (!this._tags)
|
|
31579
|
+
return;
|
|
31580
|
+
const idx = this._tags.findIndex((t) => t.tag_id === tag.tag_id);
|
|
31581
|
+
if (idx >= 0) {
|
|
31582
|
+
Object.assign(this._tags[idx], tag);
|
|
31583
|
+
} else {
|
|
31584
|
+
this._tags.push(tag);
|
|
31585
|
+
}
|
|
31586
|
+
}
|
|
31587
|
+
patchCachedTagDelete(tagId) {
|
|
31588
|
+
if (!this._tags)
|
|
31589
|
+
return false;
|
|
31590
|
+
const before = this._tags.length;
|
|
31591
|
+
this._tags = this._tags.filter((t) => t.tag_id !== tagId);
|
|
31592
|
+
return this._tags.length < before;
|
|
31593
|
+
}
|
|
31594
|
+
patchCachedCategoryUpsert(category) {
|
|
31595
|
+
if (!this._userCategories)
|
|
31596
|
+
return;
|
|
31597
|
+
const idx = this._userCategories.findIndex((c) => c.category_id === category.category_id);
|
|
31598
|
+
if (idx >= 0) {
|
|
31599
|
+
Object.assign(this._userCategories[idx], category);
|
|
31600
|
+
} else {
|
|
31601
|
+
this._userCategories.push(category);
|
|
31602
|
+
}
|
|
31603
|
+
this._categoryNameMap = null;
|
|
31604
|
+
}
|
|
31605
|
+
patchCachedCategoryDelete(categoryId) {
|
|
31606
|
+
if (!this._userCategories)
|
|
31607
|
+
return false;
|
|
31608
|
+
const before = this._userCategories.length;
|
|
31609
|
+
this._userCategories = this._userCategories.filter((c) => c.category_id !== categoryId);
|
|
31610
|
+
if (this._userCategories.length < before) {
|
|
31611
|
+
this._categoryNameMap = null;
|
|
31612
|
+
return true;
|
|
31613
|
+
}
|
|
31614
|
+
return false;
|
|
31615
|
+
}
|
|
31616
|
+
patchCachedRecurringUpsert(recurring) {
|
|
31617
|
+
if (!this._recurring)
|
|
31618
|
+
return;
|
|
31619
|
+
const idx = this._recurring.findIndex((r) => r.recurring_id === recurring.recurring_id);
|
|
31620
|
+
if (idx >= 0) {
|
|
31621
|
+
Object.assign(this._recurring[idx], recurring);
|
|
31622
|
+
} else {
|
|
31623
|
+
this._recurring.push(recurring);
|
|
31624
|
+
}
|
|
31625
|
+
}
|
|
31626
|
+
patchCachedRecurringDelete(recurringId) {
|
|
31627
|
+
if (!this._recurring)
|
|
31628
|
+
return false;
|
|
31629
|
+
const before = this._recurring.length;
|
|
31630
|
+
this._recurring = this._recurring.filter((r) => r.recurring_id !== recurringId);
|
|
31631
|
+
return this._recurring.length < before;
|
|
31632
|
+
}
|
|
31555
31633
|
getCacheLoadedAt() {
|
|
31556
31634
|
return this._cacheLoadedAt;
|
|
31557
31635
|
}
|
|
@@ -32568,14 +32646,6 @@ var CREATE_RECURRING = `mutation CreateRecurring($input: CreateRecurringInput!)
|
|
|
32568
32646
|
createRecurring(input: $input) {
|
|
32569
32647
|
__typename
|
|
32570
32648
|
...RecurringFields
|
|
32571
|
-
rule {
|
|
32572
|
-
__typename
|
|
32573
|
-
...RecurringRuleFields
|
|
32574
|
-
}
|
|
32575
|
-
payments {
|
|
32576
|
-
__typename
|
|
32577
|
-
...RecurringPaymentFields
|
|
32578
|
-
}
|
|
32579
32649
|
}
|
|
32580
32650
|
}
|
|
32581
32651
|
|
|
@@ -32601,21 +32671,6 @@ fragment RecurringFields on Recurring {
|
|
|
32601
32671
|
state
|
|
32602
32672
|
name
|
|
32603
32673
|
id
|
|
32604
|
-
}
|
|
32605
|
-
|
|
32606
|
-
fragment RecurringRuleFields on RecurringRule {
|
|
32607
|
-
__typename
|
|
32608
|
-
nameContains
|
|
32609
|
-
minAmount
|
|
32610
|
-
maxAmount
|
|
32611
|
-
days
|
|
32612
|
-
}
|
|
32613
|
-
|
|
32614
|
-
fragment RecurringPaymentFields on RecurringPayment {
|
|
32615
|
-
__typename
|
|
32616
|
-
amount
|
|
32617
|
-
isPaid
|
|
32618
|
-
date
|
|
32619
32674
|
}`;
|
|
32620
32675
|
var EDIT_RECURRING = `mutation EditRecurring($id: ID!, $input: EditRecurringInput!) {
|
|
32621
32676
|
editRecurring(id: $id, input: $input) {
|
|
@@ -32623,14 +32678,6 @@ var EDIT_RECURRING = `mutation EditRecurring($id: ID!, $input: EditRecurringInpu
|
|
|
32623
32678
|
recurring {
|
|
32624
32679
|
__typename
|
|
32625
32680
|
...RecurringFields
|
|
32626
|
-
rule {
|
|
32627
|
-
__typename
|
|
32628
|
-
...RecurringRuleFields
|
|
32629
|
-
}
|
|
32630
|
-
payments {
|
|
32631
|
-
__typename
|
|
32632
|
-
...RecurringPaymentFields
|
|
32633
|
-
}
|
|
32634
32681
|
}
|
|
32635
32682
|
}
|
|
32636
32683
|
}
|
|
@@ -32657,21 +32704,6 @@ fragment RecurringFields on Recurring {
|
|
|
32657
32704
|
state
|
|
32658
32705
|
name
|
|
32659
32706
|
id
|
|
32660
|
-
}
|
|
32661
|
-
|
|
32662
|
-
fragment RecurringRuleFields on RecurringRule {
|
|
32663
|
-
__typename
|
|
32664
|
-
nameContains
|
|
32665
|
-
minAmount
|
|
32666
|
-
maxAmount
|
|
32667
|
-
days
|
|
32668
|
-
}
|
|
32669
|
-
|
|
32670
|
-
fragment RecurringPaymentFields on RecurringPayment {
|
|
32671
|
-
__typename
|
|
32672
|
-
amount
|
|
32673
|
-
isPaid
|
|
32674
|
-
date
|
|
32675
32707
|
}`;
|
|
32676
32708
|
var DELETE_RECURRING = `mutation DeleteRecurring($deleteRecurringId: ID!) {
|
|
32677
32709
|
deleteRecurring(id: $deleteRecurringId)
|
|
@@ -32802,20 +32834,8 @@ async function editRecurring(client, args) {
|
|
|
32802
32834
|
const changed = {};
|
|
32803
32835
|
if ("state" in args.input)
|
|
32804
32836
|
changed.state = recurring.state;
|
|
32805
|
-
if ("rule" in args.input &&
|
|
32806
|
-
|
|
32807
|
-
const stringRule = {};
|
|
32808
|
-
if ("nameContains" in serverRule)
|
|
32809
|
-
stringRule.nameContains = serverRule.nameContains;
|
|
32810
|
-
if ("minAmount" in serverRule && serverRule.minAmount !== undefined) {
|
|
32811
|
-
stringRule.minAmount = String(serverRule.minAmount);
|
|
32812
|
-
}
|
|
32813
|
-
if ("maxAmount" in serverRule && serverRule.maxAmount !== undefined) {
|
|
32814
|
-
stringRule.maxAmount = String(serverRule.maxAmount);
|
|
32815
|
-
}
|
|
32816
|
-
if ("days" in serverRule)
|
|
32817
|
-
stringRule.days = serverRule.days;
|
|
32818
|
-
changed.rule = stringRule;
|
|
32837
|
+
if ("rule" in args.input && args.input.rule) {
|
|
32838
|
+
changed.rule = { ...args.input.rule };
|
|
32819
32839
|
}
|
|
32820
32840
|
return { id: recurring.id, changed };
|
|
32821
32841
|
}
|
|
@@ -33855,23 +33875,32 @@ class CopilotMoneyTools {
|
|
|
33855
33875
|
async getBudgets(options = {}) {
|
|
33856
33876
|
const { active_only = false } = options;
|
|
33857
33877
|
const allBudgets = await this.db.getBudgets(active_only);
|
|
33878
|
+
const now = new Date;
|
|
33879
|
+
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
33880
|
+
const effectiveAmount = (b) => {
|
|
33881
|
+
const override = b.amounts?.[currentMonth];
|
|
33882
|
+
return override !== undefined ? override : b.amount;
|
|
33883
|
+
};
|
|
33884
|
+
const nonTombstone = allBudgets.filter((b) => b.category_id !== undefined || b.amount !== undefined || b.amounts !== undefined);
|
|
33858
33885
|
const categoryMap2 = await this.getUserCategoryMap();
|
|
33859
|
-
const budgets =
|
|
33886
|
+
const budgets = nonTombstone.filter((b) => {
|
|
33860
33887
|
if (!b.category_id)
|
|
33861
33888
|
return true;
|
|
33862
33889
|
return categoryMap2.has(b.category_id) || isKnownPlaidCategory(b.category_id);
|
|
33863
33890
|
});
|
|
33864
33891
|
let totalBudgeted = 0;
|
|
33865
33892
|
for (const budget of budgets) {
|
|
33866
|
-
|
|
33867
|
-
|
|
33893
|
+
const amt = effectiveAmount(budget);
|
|
33894
|
+
if (amt) {
|
|
33895
|
+
const monthlyAmount = budget.period === "yearly" ? amt / 12 : budget.period === "weekly" ? amt * 4.33 : budget.period === "daily" ? amt * 30 : amt;
|
|
33868
33896
|
totalBudgeted += monthlyAmount;
|
|
33869
33897
|
}
|
|
33870
33898
|
}
|
|
33871
33899
|
const enrichedBudgets = await Promise.all(budgets.map(async (b) => ({
|
|
33872
33900
|
budget_id: b.budget_id,
|
|
33873
33901
|
name: b.name,
|
|
33874
|
-
amount: b
|
|
33902
|
+
amount: effectiveAmount(b),
|
|
33903
|
+
...b.amounts ? { amounts: b.amounts } : {},
|
|
33875
33904
|
period: b.period,
|
|
33876
33905
|
category_id: b.category_id,
|
|
33877
33906
|
category_name: b.category_id ? await this.resolveCategoryName(b.category_id) : undefined,
|
|
@@ -34078,6 +34107,13 @@ class CopilotMoneyTools {
|
|
|
34078
34107
|
isExcluded: args.is_excluded ?? false
|
|
34079
34108
|
}
|
|
34080
34109
|
});
|
|
34110
|
+
this.db.patchCachedCategoryUpsert({
|
|
34111
|
+
category_id: result.id,
|
|
34112
|
+
name: result.name,
|
|
34113
|
+
color: result.colorName,
|
|
34114
|
+
emoji: args.emoji,
|
|
34115
|
+
excluded: args.is_excluded ?? false
|
|
34116
|
+
});
|
|
34081
34117
|
return {
|
|
34082
34118
|
success: true,
|
|
34083
34119
|
category_id: result.id,
|
|
@@ -34153,6 +34189,15 @@ class CopilotMoneyTools {
|
|
|
34153
34189
|
isReviewed: "reviewed"
|
|
34154
34190
|
};
|
|
34155
34191
|
const updated = Object.keys(result.changed).map((k) => graphqlToApiName[k] ?? k);
|
|
34192
|
+
const patch = {};
|
|
34193
|
+
if ("category_id" in args && args.category_id !== undefined)
|
|
34194
|
+
patch.category_id = args.category_id;
|
|
34195
|
+
if ("note" in args && args.note !== undefined)
|
|
34196
|
+
patch.user_note = args.note;
|
|
34197
|
+
if ("tag_ids" in args && args.tag_ids !== undefined)
|
|
34198
|
+
patch.tag_ids = args.tag_ids;
|
|
34199
|
+
if (Object.keys(patch).length > 0)
|
|
34200
|
+
this.db.patchCachedTransaction(transaction_id, patch);
|
|
34156
34201
|
return {
|
|
34157
34202
|
success: true,
|
|
34158
34203
|
transaction_id: result.id,
|
|
@@ -34223,6 +34268,9 @@ class CopilotMoneyTools {
|
|
|
34223
34268
|
}
|
|
34224
34269
|
throw error48;
|
|
34225
34270
|
}
|
|
34271
|
+
for (const id of transaction_ids) {
|
|
34272
|
+
this.db.patchCachedTransaction(id, { user_reviewed: reviewed });
|
|
34273
|
+
}
|
|
34226
34274
|
return {
|
|
34227
34275
|
success: true,
|
|
34228
34276
|
reviewed_count,
|
|
@@ -34238,6 +34286,11 @@ class CopilotMoneyTools {
|
|
|
34238
34286
|
const result = await createTag(client, {
|
|
34239
34287
|
input: { name: args.name.trim(), colorName }
|
|
34240
34288
|
});
|
|
34289
|
+
this.db.patchCachedTagUpsert({
|
|
34290
|
+
tag_id: result.id,
|
|
34291
|
+
name: result.name,
|
|
34292
|
+
color_name: result.colorName
|
|
34293
|
+
});
|
|
34241
34294
|
return {
|
|
34242
34295
|
success: true,
|
|
34243
34296
|
tag_id: result.id,
|
|
@@ -34254,6 +34307,7 @@ class CopilotMoneyTools {
|
|
|
34254
34307
|
const client = this.getGraphQLClient();
|
|
34255
34308
|
try {
|
|
34256
34309
|
const result = await deleteTag(client, { id: args.tag_id });
|
|
34310
|
+
this.db.patchCachedTagDelete(args.tag_id);
|
|
34257
34311
|
return { success: true, tag_id: result.id, deleted: true };
|
|
34258
34312
|
} catch (e) {
|
|
34259
34313
|
if (e instanceof GraphQLError)
|
|
@@ -34277,6 +34331,16 @@ class CopilotMoneyTools {
|
|
|
34277
34331
|
}
|
|
34278
34332
|
try {
|
|
34279
34333
|
const result = await editCategory(client, { id: args.category_id, input });
|
|
34334
|
+
const patch = { category_id: args.category_id };
|
|
34335
|
+
if (args.name !== undefined)
|
|
34336
|
+
patch.name = args.name;
|
|
34337
|
+
if (args.color_name !== undefined)
|
|
34338
|
+
patch.color = args.color_name;
|
|
34339
|
+
if (args.emoji !== undefined)
|
|
34340
|
+
patch.emoji = args.emoji;
|
|
34341
|
+
if (args.is_excluded !== undefined)
|
|
34342
|
+
patch.excluded = args.is_excluded;
|
|
34343
|
+
this.db.patchCachedCategoryUpsert(patch);
|
|
34280
34344
|
return {
|
|
34281
34345
|
success: true,
|
|
34282
34346
|
category_id: result.id,
|
|
@@ -34292,6 +34356,7 @@ class CopilotMoneyTools {
|
|
|
34292
34356
|
const client = this.getGraphQLClient();
|
|
34293
34357
|
try {
|
|
34294
34358
|
const result = await deleteCategory(client, { id: args.category_id });
|
|
34359
|
+
this.db.patchCachedCategoryDelete(args.category_id);
|
|
34295
34360
|
return { success: true, category_id: result.id, deleted: true };
|
|
34296
34361
|
} catch (e) {
|
|
34297
34362
|
if (e instanceof GraphQLError)
|
|
@@ -34318,6 +34383,7 @@ class CopilotMoneyTools {
|
|
|
34318
34383
|
amount: args.amount,
|
|
34319
34384
|
month: args.month
|
|
34320
34385
|
});
|
|
34386
|
+
this.db.patchCachedBudget(args.category_id, parseFloat(args.amount), args.month);
|
|
34321
34387
|
return {
|
|
34322
34388
|
success: true,
|
|
34323
34389
|
category_id: result.categoryId,
|
|
@@ -34342,6 +34408,10 @@ class CopilotMoneyTools {
|
|
|
34342
34408
|
id: args.recurring_id,
|
|
34343
34409
|
input: { state: args.state }
|
|
34344
34410
|
});
|
|
34411
|
+
this.db.patchCachedRecurringUpsert({
|
|
34412
|
+
recurring_id: args.recurring_id,
|
|
34413
|
+
state: args.state.toLowerCase()
|
|
34414
|
+
});
|
|
34345
34415
|
return { success: true, recurring_id: result.id, state: args.state };
|
|
34346
34416
|
} catch (e) {
|
|
34347
34417
|
if (e instanceof GraphQLError)
|
|
@@ -34353,6 +34423,7 @@ class CopilotMoneyTools {
|
|
|
34353
34423
|
const client = this.getGraphQLClient();
|
|
34354
34424
|
try {
|
|
34355
34425
|
const result = await deleteRecurring(client, { id: args.recurring_id });
|
|
34426
|
+
this.db.patchCachedRecurringDelete(args.recurring_id);
|
|
34356
34427
|
return { success: true, recurring_id: result.id, deleted: true };
|
|
34357
34428
|
} catch (e) {
|
|
34358
34429
|
if (e instanceof GraphQLError)
|
|
@@ -34372,6 +34443,12 @@ class CopilotMoneyTools {
|
|
|
34372
34443
|
}
|
|
34373
34444
|
try {
|
|
34374
34445
|
const result = await editTag(client, { id: args.tag_id, input });
|
|
34446
|
+
const patch = { tag_id: args.tag_id };
|
|
34447
|
+
if (args.name !== undefined)
|
|
34448
|
+
patch.name = args.name;
|
|
34449
|
+
if (args.color_name !== undefined)
|
|
34450
|
+
patch.color_name = args.color_name;
|
|
34451
|
+
this.db.patchCachedTagUpsert(patch);
|
|
34375
34452
|
return { success: true, tag_id: result.id, updated: Object.keys(result.changed) };
|
|
34376
34453
|
} catch (e) {
|
|
34377
34454
|
if (e instanceof GraphQLError)
|
|
@@ -34403,6 +34480,12 @@ class CopilotMoneyTools {
|
|
|
34403
34480
|
}
|
|
34404
34481
|
}
|
|
34405
34482
|
});
|
|
34483
|
+
this.db.patchCachedRecurringUpsert({
|
|
34484
|
+
recurring_id: result.id,
|
|
34485
|
+
name: result.name,
|
|
34486
|
+
state: result.state.toLowerCase(),
|
|
34487
|
+
frequency: result.frequency
|
|
34488
|
+
});
|
|
34406
34489
|
return {
|
|
34407
34490
|
success: true,
|
|
34408
34491
|
recurring_id: result.id,
|
|
@@ -34438,6 +34521,17 @@ class CopilotMoneyTools {
|
|
|
34438
34521
|
}
|
|
34439
34522
|
try {
|
|
34440
34523
|
const result = await editRecurring(client, { id: args.recurring_id, input });
|
|
34524
|
+
const patch = { recurring_id: args.recurring_id };
|
|
34525
|
+
if (args.state !== undefined) {
|
|
34526
|
+
patch.state = args.state.toLowerCase();
|
|
34527
|
+
}
|
|
34528
|
+
if (args.rule?.name_contains !== undefined)
|
|
34529
|
+
patch.match_string = args.rule.name_contains;
|
|
34530
|
+
if (args.rule?.min_amount !== undefined)
|
|
34531
|
+
patch.min_amount = parseFloat(args.rule.min_amount);
|
|
34532
|
+
if (args.rule?.max_amount !== undefined)
|
|
34533
|
+
patch.max_amount = parseFloat(args.rule.max_amount);
|
|
34534
|
+
this.db.patchCachedRecurringUpsert(patch);
|
|
34441
34535
|
return { success: true, recurring_id: result.id, updated: Object.keys(result.changed) };
|
|
34442
34536
|
} catch (e) {
|
|
34443
34537
|
if (e instanceof GraphQLError)
|
|
@@ -34894,7 +34988,7 @@ function createToolSchemas() {
|
|
|
34894
34988
|
},
|
|
34895
34989
|
{
|
|
34896
34990
|
name: "get_budgets",
|
|
34897
|
-
description: "Get budgets from Copilot's native budget tracking. " + "
|
|
34991
|
+
description: "Get budgets from Copilot's native budget tracking. " + "Returns the current-month effective budget per category plus the full " + "`amounts` map of per-month overrides for history lookups. For parent " + "categories, the returned `amount` is the resolved total (children + " + "rollovers) that Copilot displays in the Budgets view. Totals use the " + "current-month effective amount.",
|
|
34898
34992
|
inputSchema: {
|
|
34899
34993
|
type: "object",
|
|
34900
34994
|
properties: {
|
|
@@ -35015,7 +35109,7 @@ function createToolSchemas() {
|
|
|
35015
35109
|
},
|
|
35016
35110
|
{
|
|
35017
35111
|
name: "get_balance_history",
|
|
35018
|
-
description: "Get daily balance snapshots for accounts over time.
|
|
35112
|
+
description: "Get daily balance snapshots for accounts over time. Each entry returns current_balance, " + "available_balance, limit, account_id, and account_name. The response also includes an " + "`accounts` array listing the distinct account IDs in the paginated page. Requires a " + "granularity parameter (daily, weekly, or monthly) to control response size. Weekly and " + "monthly modes downsample by keeping the last data point per period. Filter by " + "account_id and date range.",
|
|
35019
35113
|
inputSchema: {
|
|
35020
35114
|
type: "object",
|
|
35021
35115
|
properties: {
|
|
@@ -35214,7 +35308,7 @@ function createWriteToolSchemas() {
|
|
|
35214
35308
|
},
|
|
35215
35309
|
{
|
|
35216
35310
|
name: "review_transactions",
|
|
35217
|
-
description: "Mark one or more transactions as reviewed (or unreviewed). " + "Accepts an array of transaction_ids. Writes
|
|
35311
|
+
description: "Mark one or more transactions as reviewed (or unreviewed). " + "Accepts an array of transaction_ids. Writes are issued via GraphQL in parallel with " + "a cap of 5 in flight at a time. On the first GraphQL error, new writes stop, in-flight " + "writes settle, and the error is thrown with a `reviewed_count` reflecting how many " + "succeeded before the failure (partial success is possible).",
|
|
35218
35312
|
inputSchema: {
|
|
35219
35313
|
type: "object",
|
|
35220
35314
|
properties: {
|
|
@@ -35248,11 +35342,7 @@ function createWriteToolSchemas() {
|
|
|
35248
35342
|
},
|
|
35249
35343
|
color_name: {
|
|
35250
35344
|
type: "string",
|
|
35251
|
-
description: 'Optional
|
|
35252
|
-
},
|
|
35253
|
-
hex_color: {
|
|
35254
|
-
type: "string",
|
|
35255
|
-
description: 'Optional hex color code (e.g. "#FF5733")'
|
|
35345
|
+
description: 'Optional palette token from Copilot (e.g. "PURPLE2", "OLIVE1", "RED1"). ' + 'Defaults to "PURPLE2" when omitted. See existing tags for valid values.'
|
|
35256
35346
|
}
|
|
35257
35347
|
},
|
|
35258
35348
|
required: ["name"]
|
|
@@ -35372,7 +35462,7 @@ function createWriteToolSchemas() {
|
|
|
35372
35462
|
},
|
|
35373
35463
|
{
|
|
35374
35464
|
name: "set_budget",
|
|
35375
|
-
description: 'Set the monthly budget amount for a category. amount="0" clears the budget. ' + 'Pass month="YYYY-MM" for a single-month override; omit for the all-months default. ' + 'Note: if the user has disabled "Enable budgeting" or "Enable rollover" in ' + "Copilot → Settings → General, the budget write still succeeds on the server, but " + "the value will not appear in the Copilot UI until those toggles are re-enabled. " + 'Rollover behavior also depends on the "Rollover categories" selection in the same ' + "settings pane, which is not writable through this tool.
|
|
35465
|
+
description: 'Set the monthly budget amount for a category. amount="0" clears the budget. ' + 'Pass month="YYYY-MM" for a single-month override; omit for the all-months default. ' + 'Note: if the user has disabled "Enable budgeting" or "Enable rollover" in ' + "Copilot → Settings → General, the budget write still succeeds on the server, but " + "the value will not appear in the Copilot UI until those toggles are re-enabled. " + 'Rollover behavior also depends on the "Rollover categories" selection in the same ' + "settings pane, which is not writable through this tool.",
|
|
35376
35466
|
inputSchema: {
|
|
35377
35467
|
type: "object",
|
|
35378
35468
|
properties: {
|
|
@@ -35443,7 +35533,7 @@ function createWriteToolSchemas() {
|
|
|
35443
35533
|
},
|
|
35444
35534
|
{
|
|
35445
35535
|
name: "update_tag",
|
|
35446
|
-
description: "Update an existing tag. Provide tag_id (required) and at least one of name
|
|
35536
|
+
description: "Update an existing tag. Provide tag_id (required) and at least one of name or " + "color_name. Only the specified fields are updated. " + "Writes directly to Copilot Money via GraphQL.",
|
|
35447
35537
|
inputSchema: {
|
|
35448
35538
|
type: "object",
|
|
35449
35539
|
properties: {
|
|
@@ -35457,11 +35547,7 @@ function createWriteToolSchemas() {
|
|
|
35457
35547
|
},
|
|
35458
35548
|
color_name: {
|
|
35459
35549
|
type: "string",
|
|
35460
|
-
description: 'New
|
|
35461
|
-
},
|
|
35462
|
-
hex_color: {
|
|
35463
|
-
type: "string",
|
|
35464
|
-
description: 'New hex color code (e.g. "#FF5733")'
|
|
35550
|
+
description: 'New palette token from Copilot (e.g. "PURPLE2", "OLIVE1", "RED1"). ' + "See existing tags for valid values."
|
|
35465
35551
|
}
|
|
35466
35552
|
},
|
|
35467
35553
|
required: ["tag_id"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-money-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "MCP server for Copilot Money — query personal finances locally (17 read tools) and manage them via Copilot's GraphQL API (13 write tools, opt-in with --write)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"fix": "bun run lint:fix && bun run format",
|
|
64
64
|
"clean": "rm -rf dist coverage .bun-build",
|
|
65
65
|
"pack:mcpb": "bun run scripts/pack-mcpb.ts",
|
|
66
|
+
"pack:mcpb:write": "bun run scripts/sync-manifest.ts -- --write && bun run scripts/pack-mcpb.ts -- --write",
|
|
66
67
|
"prepublishOnly": "bun run clean && bun run build && bun test",
|
|
67
68
|
"prepare": "husky"
|
|
68
69
|
},
|