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 CHANGED
@@ -4,17 +4,14 @@
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node.js 18+](https://img.shields.io/badge/node-18+-green.svg)](https://nodejs.org/)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/)
8
- [![Tests](https://img.shields.io/badge/tests-1400+-brightgreen.svg)](https://github.com/ignaciohermosillacornejo/copilot-money-mcp)
9
- [![Tools](https://img.shields.io/badge/tools-17-blue.svg)](https://github.com/ignaciohermosillacornejo/copilot-money-mcp)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue.svg)](https://www.typescriptlang.org/)
8
+ [![Tests](https://github.com/ignaciohermosillacornejo/copilot-money-mcp/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/ignaciohermosillacornejo/copilot-money-mcp/actions/workflows/test.yml)
9
+ [![codecov](https://codecov.io/gh/ignaciohermosillacornejo/copilot-money-mcp/branch/main/graph/badge.svg)](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 && recurring.rule) {
32807
- const serverRule = recurring.rule;
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 = allBudgets.filter((b) => {
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
- if (budget.amount) {
33868
- const monthlyAmount = budget.period === "yearly" ? budget.amount / 12 : budget.period === "weekly" ? budget.amount * 4.33 : budget.period === "daily" ? budget.amount * 30 : budget.amount;
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.amount,
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. " + "Retrieves user-defined spending limits and budget rules stored in the app. " + "Returns budget details including amounts, periods (monthly/yearly/weekly), " + "category associations, and active status. Calculates total budgeted amount as monthly equivalent. " + "Sync note: after `set_budget` writes, budget changes can take significantly longer " + "to reflect in this read than other collections (transactions/tags/categories/recurrings " + "sync in seconds; budgets may take minutes). Do not assume a fresh `set_budget` is " + "immediately observable here — poll with `refresh_database` or verify directly in the " + "Copilot app. Per-month overrides written via `set_budget(month=...)` are not surfaced " + "in this view; only the all-months default `amount` is returned.",
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. Returns current_balance, " + "available_balance, and limit per day. 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.",
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 directly to Copilot Money via GraphQL.",
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 color name (e.g. "blue", "red")'
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. " + "Sync delay: successful writes may not be visible via `get_budgets` for minutes — " + "budget docs sync on a slower cadence than other collections. Do not retry the write " + "just because the read does not reflect it yet.",
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, " + "color_name, or hex_color. Only the specified fields are updated. " + "Writes directly to Copilot Money via GraphQL.",
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 color name (e.g. "blue", "red")'
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, false);
36140
+ await runServer(dbPath, timeoutMs, writeFlagSeen);
36054
36141
  } catch (error48) {
36055
36142
  console.error("Server error:", error48);
36056
36143
  process.exit(1);
@@ -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 && recurring.rule) {
32806
- const serverRule = recurring.rule;
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 = allBudgets.filter((b) => {
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
- if (budget.amount) {
33867
- const monthlyAmount = budget.period === "yearly" ? budget.amount / 12 : budget.period === "weekly" ? budget.amount * 4.33 : budget.period === "daily" ? budget.amount * 30 : budget.amount;
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.amount,
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. " + "Retrieves user-defined spending limits and budget rules stored in the app. " + "Returns budget details including amounts, periods (monthly/yearly/weekly), " + "category associations, and active status. Calculates total budgeted amount as monthly equivalent. " + "Sync note: after `set_budget` writes, budget changes can take significantly longer " + "to reflect in this read than other collections (transactions/tags/categories/recurrings " + "sync in seconds; budgets may take minutes). Do not assume a fresh `set_budget` is " + "immediately observable here — poll with `refresh_database` or verify directly in the " + "Copilot app. Per-month overrides written via `set_budget(month=...)` are not surfaced " + "in this view; only the all-months default `amount` is returned.",
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. Returns current_balance, " + "available_balance, and limit per day. 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.",
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 directly to Copilot Money via GraphQL.",
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 color name (e.g. "blue", "red")'
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. " + "Sync delay: successful writes may not be visible via `get_budgets` for minutes — " + "budget docs sync on a slower cadence than other collections. Do not retry the write " + "just because the read does not reflect it yet.",
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, " + "color_name, or hex_color. Only the specified fields are updated. " + "Writes directly to Copilot Money via GraphQL.",
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 color name (e.g. "blue", "red")'
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.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
  },