splitwise-mcp 2.0.12 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +44 -35
- package/dist/client.js +21 -7
- package/dist/index.js +12 -6
- package/dist/splitwise-auth.js +23 -0
- package/dist/tools/expenses.js +1 -2
- package/dist/tools/friends.js +1 -2
- package/dist/tools/groups.js +1 -2
- package/dist/tools/user.js +1 -2
- package/dist/tools/utilities.js +1 -2
- package/package.json +12 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Splitwise — natural-language expense and group management via the Splitwise API",
|
|
10
|
-
"version": "2.0
|
|
10
|
+
"version": "2.1.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Splitwise",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Splitwise — manage expenses, groups, and friends via natural language",
|
|
18
|
-
"version": "2.0
|
|
18
|
+
"version": "2.1.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -31282,8 +31282,11 @@ var pageSchema = {
|
|
|
31282
31282
|
// src/client.ts
|
|
31283
31283
|
import { dirname, join } from "path";
|
|
31284
31284
|
import { fileURLToPath } from "url";
|
|
31285
|
-
|
|
31286
|
-
|
|
31285
|
+
try {
|
|
31286
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
31287
|
+
await loadDotenvSafely({ path: join(dir, "..", ".env"), override: false });
|
|
31288
|
+
} catch {
|
|
31289
|
+
}
|
|
31287
31290
|
var BASE_URL = "https://secure.splitwise.com/api/v3.0";
|
|
31288
31291
|
var SERVICE_NAME = "Splitwise";
|
|
31289
31292
|
var SplitwiseClient = class {
|
|
@@ -31294,9 +31297,14 @@ var SplitwiseClient = class {
|
|
|
31294
31297
|
* Defer the config error so the server can still start (and respond to the
|
|
31295
31298
|
* host's install-time smoke test) when SPLITWISE_API_KEY isn't set yet.
|
|
31296
31299
|
* Tool calls re-raise the error at request time.
|
|
31300
|
+
*
|
|
31301
|
+
* Optional constructor seam: a hosted per-user deployment (e.g. a Cloudflare
|
|
31302
|
+
* Worker "remote connector") builds one client per request with that user's
|
|
31303
|
+
* `apiKey` injected. The stdio path passes no options, so the key resolves
|
|
31304
|
+
* from the environment exactly as before — byte-for-byte identical behaviour.
|
|
31297
31305
|
*/
|
|
31298
|
-
constructor() {
|
|
31299
|
-
const key = readEnvVar("SPLITWISE_API_KEY");
|
|
31306
|
+
constructor(opts) {
|
|
31307
|
+
const key = opts?.apiKey ?? readEnvVar("SPLITWISE_API_KEY");
|
|
31300
31308
|
if (!key) {
|
|
31301
31309
|
this.apiKey = null;
|
|
31302
31310
|
this.configError = new Error("SPLITWISE_API_KEY environment variable is required");
|
|
@@ -31338,12 +31346,12 @@ function previewUnlessConfirmed(confirm, action, method, path, body) {
|
|
|
31338
31346
|
}
|
|
31339
31347
|
|
|
31340
31348
|
// src/tools/user.ts
|
|
31341
|
-
function registerUserTools(server) {
|
|
31349
|
+
function registerUserTools(server, client2) {
|
|
31342
31350
|
server.registerTool("sw_get_current_user", {
|
|
31343
31351
|
description: "Get the authenticated Splitwise user's profile (id, first_name, last_name, email). Use the returned id when building custom expense splits.",
|
|
31344
31352
|
annotations: { readOnlyHint: true }
|
|
31345
31353
|
}, async () => {
|
|
31346
|
-
const data = await
|
|
31354
|
+
const data = await client2.request("GET", "/get_current_user");
|
|
31347
31355
|
return textResult(data);
|
|
31348
31356
|
});
|
|
31349
31357
|
server.registerTool("sw_get_user", {
|
|
@@ -31353,7 +31361,7 @@ function registerUserTools(server) {
|
|
|
31353
31361
|
id: external_exports.number().describe("User ID")
|
|
31354
31362
|
}
|
|
31355
31363
|
}, async ({ id }) => {
|
|
31356
|
-
const data = await
|
|
31364
|
+
const data = await client2.request("GET", `/get_user/${id}`);
|
|
31357
31365
|
return textResult(data);
|
|
31358
31366
|
});
|
|
31359
31367
|
server.registerTool("sw_update_user", {
|
|
@@ -31379,18 +31387,18 @@ function registerUserTools(server) {
|
|
|
31379
31387
|
const previewBody = { ...body, ...password !== void 0 ? { password: "[hidden]" } : {} };
|
|
31380
31388
|
const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, "POST", `/update_user/${id}`, previewBody);
|
|
31381
31389
|
if (gate) return gate;
|
|
31382
|
-
const data = await
|
|
31390
|
+
const data = await client2.request("POST", `/update_user/${id}`, body);
|
|
31383
31391
|
return textResult(data);
|
|
31384
31392
|
});
|
|
31385
31393
|
}
|
|
31386
31394
|
|
|
31387
31395
|
// src/tools/groups.ts
|
|
31388
|
-
function registerGroupTools(server) {
|
|
31396
|
+
function registerGroupTools(server, client2) {
|
|
31389
31397
|
server.registerTool("sw_list_groups", {
|
|
31390
31398
|
description: "List all Splitwise groups the current user belongs to. Returns id, name, and members for each group. Use this to resolve a group name to its id.",
|
|
31391
31399
|
annotations: { readOnlyHint: true }
|
|
31392
31400
|
}, async () => {
|
|
31393
|
-
const data = await
|
|
31401
|
+
const data = await client2.request("GET", "/get_groups");
|
|
31394
31402
|
return textResult(data);
|
|
31395
31403
|
});
|
|
31396
31404
|
server.registerTool("sw_get_group", {
|
|
@@ -31400,7 +31408,7 @@ function registerGroupTools(server) {
|
|
|
31400
31408
|
id: external_exports.number().describe("Group ID")
|
|
31401
31409
|
}
|
|
31402
31410
|
}, async ({ id }) => {
|
|
31403
|
-
const data = await
|
|
31411
|
+
const data = await client2.request("GET", `/get_group/${id}`);
|
|
31404
31412
|
return textResult(data);
|
|
31405
31413
|
});
|
|
31406
31414
|
server.registerTool("sw_create_group", {
|
|
@@ -31418,7 +31426,7 @@ function registerGroupTools(server) {
|
|
|
31418
31426
|
if (simplify_by_default !== void 0) body.simplify_by_default = simplify_by_default;
|
|
31419
31427
|
const gate = previewUnlessConfirmed(confirm, `Create Splitwise group "${name}"`, "POST", "/create_group", body);
|
|
31420
31428
|
if (gate) return gate;
|
|
31421
|
-
const data = await
|
|
31429
|
+
const data = await client2.request("POST", "/create_group", body);
|
|
31422
31430
|
return textResult(data);
|
|
31423
31431
|
});
|
|
31424
31432
|
server.registerTool("sw_add_user_to_group", {
|
|
@@ -31443,7 +31451,7 @@ function registerGroupTools(server) {
|
|
|
31443
31451
|
}
|
|
31444
31452
|
const gate = previewUnlessConfirmed(confirm, `Add a user to Splitwise group ${group_id} (may send an invite email)`, "POST", "/add_user_to_group", body);
|
|
31445
31453
|
if (gate) return gate;
|
|
31446
|
-
const data = await
|
|
31454
|
+
const data = await client2.request("POST", "/add_user_to_group", body);
|
|
31447
31455
|
return textResult(data);
|
|
31448
31456
|
});
|
|
31449
31457
|
server.registerTool("sw_remove_user_from_group", {
|
|
@@ -31457,7 +31465,7 @@ function registerGroupTools(server) {
|
|
|
31457
31465
|
}, async ({ group_id, user_id, confirm }) => {
|
|
31458
31466
|
const gate = previewUnlessConfirmed(confirm, `Remove user ${user_id} from Splitwise group ${group_id}`, "POST", "/remove_user_from_group", { group_id, user_id });
|
|
31459
31467
|
if (gate) return gate;
|
|
31460
|
-
const data = await
|
|
31468
|
+
const data = await client2.request("POST", "/remove_user_from_group", { group_id, user_id });
|
|
31461
31469
|
return textResult(data);
|
|
31462
31470
|
});
|
|
31463
31471
|
server.registerTool("sw_delete_group", {
|
|
@@ -31470,7 +31478,7 @@ function registerGroupTools(server) {
|
|
|
31470
31478
|
}, async ({ id, confirm }) => {
|
|
31471
31479
|
const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise group ${id}`, "POST", `/delete_group/${id}`);
|
|
31472
31480
|
if (gate) return gate;
|
|
31473
|
-
const data = await
|
|
31481
|
+
const data = await client2.request("POST", `/delete_group/${id}`);
|
|
31474
31482
|
return textResult(data);
|
|
31475
31483
|
});
|
|
31476
31484
|
server.registerTool("sw_undelete_group", {
|
|
@@ -31479,18 +31487,18 @@ function registerGroupTools(server) {
|
|
|
31479
31487
|
id: external_exports.number().describe("Group ID to restore")
|
|
31480
31488
|
}
|
|
31481
31489
|
}, async ({ id }) => {
|
|
31482
|
-
const data = await
|
|
31490
|
+
const data = await client2.request("POST", `/undelete_group/${id}`);
|
|
31483
31491
|
return textResult(data);
|
|
31484
31492
|
});
|
|
31485
31493
|
}
|
|
31486
31494
|
|
|
31487
31495
|
// src/tools/friends.ts
|
|
31488
|
-
function registerFriendTools(server) {
|
|
31496
|
+
function registerFriendTools(server, client2) {
|
|
31489
31497
|
server.registerTool("sw_list_friends", {
|
|
31490
31498
|
description: "List all Splitwise friends with their id, first_name, last_name, and email. Use this to resolve a friend's name to a user_id before adding them to a group or building a custom expense split.",
|
|
31491
31499
|
annotations: { readOnlyHint: true }
|
|
31492
31500
|
}, async () => {
|
|
31493
|
-
const data = await
|
|
31501
|
+
const data = await client2.request("GET", "/get_friends");
|
|
31494
31502
|
return textResult(data);
|
|
31495
31503
|
});
|
|
31496
31504
|
server.registerTool("sw_create_friend", {
|
|
@@ -31508,7 +31516,7 @@ function registerFriendTools(server) {
|
|
|
31508
31516
|
if (user_last_name !== void 0) body.user_last_name = user_last_name;
|
|
31509
31517
|
const gate = previewUnlessConfirmed(confirm, `Add ${user_email} as a Splitwise friend`, "POST", "/create_friend", body);
|
|
31510
31518
|
if (gate) return gate;
|
|
31511
|
-
const data = await
|
|
31519
|
+
const data = await client2.request("POST", "/create_friend", body);
|
|
31512
31520
|
return textResult(data);
|
|
31513
31521
|
});
|
|
31514
31522
|
server.registerTool("sw_delete_friend", {
|
|
@@ -31521,7 +31529,7 @@ function registerFriendTools(server) {
|
|
|
31521
31529
|
}, async ({ id, confirm }) => {
|
|
31522
31530
|
const gate = previewUnlessConfirmed(confirm, `Remove Splitwise friendship with user ${id}`, "POST", `/delete_friend/${id}`);
|
|
31523
31531
|
if (gate) return gate;
|
|
31524
|
-
const data = await
|
|
31532
|
+
const data = await client2.request("POST", `/delete_friend/${id}`);
|
|
31525
31533
|
return textResult(data);
|
|
31526
31534
|
});
|
|
31527
31535
|
}
|
|
@@ -31554,7 +31562,7 @@ var userShareSchema = external_exports.object({
|
|
|
31554
31562
|
paid_share: external_exports.string().describe('Amount this user paid, e.g. "25.00"'),
|
|
31555
31563
|
owed_share: external_exports.string().describe('Amount this user owes, e.g. "12.50"')
|
|
31556
31564
|
});
|
|
31557
|
-
function registerExpenseTools(server) {
|
|
31565
|
+
function registerExpenseTools(server, client2) {
|
|
31558
31566
|
server.registerTool("sw_list_expenses", {
|
|
31559
31567
|
description: "List or search Splitwise expenses. All filters are optional. Use group_id to filter by group, dated_after/dated_before for date ranges.",
|
|
31560
31568
|
annotations: { readOnlyHint: true },
|
|
@@ -31579,7 +31587,7 @@ function registerExpenseTools(server) {
|
|
|
31579
31587
|
limit: args.limit,
|
|
31580
31588
|
offset: args.offset
|
|
31581
31589
|
});
|
|
31582
|
-
const data = await
|
|
31590
|
+
const data = await client2.request("GET", `/get_expenses${qs}`);
|
|
31583
31591
|
return textResult(data);
|
|
31584
31592
|
});
|
|
31585
31593
|
server.registerTool("sw_get_expense", {
|
|
@@ -31589,7 +31597,7 @@ function registerExpenseTools(server) {
|
|
|
31589
31597
|
id: external_exports.number().describe("Expense ID")
|
|
31590
31598
|
}
|
|
31591
31599
|
}, async ({ id }) => {
|
|
31592
|
-
const data = await
|
|
31600
|
+
const data = await client2.request("GET", `/get_expense/${id}`);
|
|
31593
31601
|
return textResult(data);
|
|
31594
31602
|
});
|
|
31595
31603
|
server.registerTool("sw_create_expense", {
|
|
@@ -31610,7 +31618,7 @@ function registerExpenseTools(server) {
|
|
|
31610
31618
|
const body = buildExpenseBody(args);
|
|
31611
31619
|
const gate = previewUnlessConfirmed(args.confirm, `Create a Splitwise expense "${args.description}" (${args.cost}) \u2014 notifies group members`, "POST", "/create_expense", body);
|
|
31612
31620
|
if (gate) return gate;
|
|
31613
|
-
const data = await
|
|
31621
|
+
const data = await client2.request("POST", "/create_expense", body);
|
|
31614
31622
|
return textResult(data);
|
|
31615
31623
|
});
|
|
31616
31624
|
server.registerTool("sw_update_expense", {
|
|
@@ -31632,7 +31640,7 @@ function registerExpenseTools(server) {
|
|
|
31632
31640
|
const body = buildExpenseBody(args);
|
|
31633
31641
|
const gate = previewUnlessConfirmed(args.confirm, `Update Splitwise expense ${expense_id} \u2014 notifies group members`, "POST", `/update_expense/${expense_id}`, body);
|
|
31634
31642
|
if (gate) return gate;
|
|
31635
|
-
const data = await
|
|
31643
|
+
const data = await client2.request("POST", `/update_expense/${expense_id}`, body);
|
|
31636
31644
|
return textResult(data);
|
|
31637
31645
|
});
|
|
31638
31646
|
server.registerTool("sw_delete_expense", {
|
|
@@ -31645,7 +31653,7 @@ function registerExpenseTools(server) {
|
|
|
31645
31653
|
}, async ({ id, confirm }) => {
|
|
31646
31654
|
const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise expense ${id}`, "POST", `/delete_expense/${id}`);
|
|
31647
31655
|
if (gate) return gate;
|
|
31648
|
-
const data = await
|
|
31656
|
+
const data = await client2.request("POST", `/delete_expense/${id}`);
|
|
31649
31657
|
return textResult(data);
|
|
31650
31658
|
});
|
|
31651
31659
|
server.registerTool("sw_undelete_expense", {
|
|
@@ -31654,32 +31662,32 @@ function registerExpenseTools(server) {
|
|
|
31654
31662
|
id: external_exports.number().describe("Expense ID to restore")
|
|
31655
31663
|
}
|
|
31656
31664
|
}, async ({ id }) => {
|
|
31657
|
-
const data = await
|
|
31665
|
+
const data = await client2.request("POST", `/undelete_expense/${id}`);
|
|
31658
31666
|
return textResult(data);
|
|
31659
31667
|
});
|
|
31660
31668
|
}
|
|
31661
31669
|
|
|
31662
31670
|
// src/tools/utilities.ts
|
|
31663
|
-
function registerUtilityTools(server) {
|
|
31671
|
+
function registerUtilityTools(server, client2) {
|
|
31664
31672
|
server.registerTool("sw_get_notifications", {
|
|
31665
31673
|
description: "Get recent Splitwise activity notifications for the current user.",
|
|
31666
31674
|
annotations: { readOnlyHint: true }
|
|
31667
31675
|
}, async () => {
|
|
31668
|
-
const data = await
|
|
31676
|
+
const data = await client2.request("GET", "/get_notifications");
|
|
31669
31677
|
return textResult(data);
|
|
31670
31678
|
});
|
|
31671
31679
|
server.registerTool("sw_get_categories", {
|
|
31672
31680
|
description: "Get the hierarchical list of Splitwise expense categories. Use the returned id as category_id when creating expenses.",
|
|
31673
31681
|
annotations: { readOnlyHint: true }
|
|
31674
31682
|
}, async () => {
|
|
31675
|
-
const data = await
|
|
31683
|
+
const data = await client2.request("GET", "/get_categories");
|
|
31676
31684
|
return textResult(data);
|
|
31677
31685
|
});
|
|
31678
31686
|
server.registerTool("sw_get_currencies", {
|
|
31679
31687
|
description: "Get all Splitwise-supported currency codes and units. Use the currency_code value when creating expenses in non-default currencies.",
|
|
31680
31688
|
annotations: { readOnlyHint: true }
|
|
31681
31689
|
}, async () => {
|
|
31682
|
-
const data = await
|
|
31690
|
+
const data = await client2.request("GET", "/get_currencies");
|
|
31683
31691
|
return textResult(data);
|
|
31684
31692
|
});
|
|
31685
31693
|
server.registerTool("sw_get_comments", {
|
|
@@ -31689,7 +31697,7 @@ function registerUtilityTools(server) {
|
|
|
31689
31697
|
expense_id: external_exports.number().describe("Expense ID to get comments for")
|
|
31690
31698
|
}
|
|
31691
31699
|
}, async ({ expense_id }) => {
|
|
31692
|
-
const data = await
|
|
31700
|
+
const data = await client2.request("GET", `/get_comments${buildQueryString({ expense_id })}`);
|
|
31693
31701
|
return textResult(data);
|
|
31694
31702
|
});
|
|
31695
31703
|
server.registerTool("sw_create_comment", {
|
|
@@ -31703,7 +31711,7 @@ function registerUtilityTools(server) {
|
|
|
31703
31711
|
}, async ({ expense_id, content, confirm }) => {
|
|
31704
31712
|
const gate = previewUnlessConfirmed(confirm, `Comment on Splitwise expense ${expense_id} (visible to participants)`, "POST", "/create_comment", { expense_id, content });
|
|
31705
31713
|
if (gate) return gate;
|
|
31706
|
-
const data = await
|
|
31714
|
+
const data = await client2.request("POST", "/create_comment", { expense_id, content });
|
|
31707
31715
|
return textResult(data);
|
|
31708
31716
|
});
|
|
31709
31717
|
server.registerTool("sw_delete_comment", {
|
|
@@ -31716,7 +31724,7 @@ function registerUtilityTools(server) {
|
|
|
31716
31724
|
}, async ({ id, confirm }) => {
|
|
31717
31725
|
const gate = previewUnlessConfirmed(confirm, `Delete Splitwise comment ${id}`, "POST", `/delete_comment/${id}`);
|
|
31718
31726
|
if (gate) return gate;
|
|
31719
|
-
const data = await
|
|
31727
|
+
const data = await client2.request("POST", `/delete_comment/${id}`);
|
|
31720
31728
|
return textResult(data);
|
|
31721
31729
|
});
|
|
31722
31730
|
}
|
|
@@ -31724,8 +31732,9 @@ function registerUtilityTools(server) {
|
|
|
31724
31732
|
// src/index.ts
|
|
31725
31733
|
await runMcp({
|
|
31726
31734
|
name: "splitwise-mcp",
|
|
31727
|
-
version: "2.0
|
|
31735
|
+
version: "2.1.0",
|
|
31728
31736
|
// x-release-please-version
|
|
31737
|
+
deps: client,
|
|
31729
31738
|
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
|
31730
31739
|
tools: [
|
|
31731
31740
|
registerUserTools,
|
package/dist/client.js
CHANGED
|
@@ -4,8 +4,16 @@ import { loadDotenvSafely, readEnvVar, createApiClient } from '@chrischall/mcp-u
|
|
|
4
4
|
// Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
|
|
5
5
|
// bundle). `loadDotenvSafely` swallows a missing dotenv module and never lets
|
|
6
6
|
// .env override a host-provided value.
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// The try/catch guards the Cloudflare Worker runtime, where `import.meta.url`
|
|
8
|
+
// is undefined and `fileURLToPath(undefined)` would throw at module init
|
|
9
|
+
// (Worker startup validation) — there is no filesystem / .env to load there.
|
|
10
|
+
try {
|
|
11
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
await loadDotenvSafely({ path: join(dir, '..', '.env'), override: false });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* non-Node runtime (Workers): no .env to load */
|
|
16
|
+
}
|
|
9
17
|
const BASE_URL = 'https://secure.splitwise.com/api/v3.0';
|
|
10
18
|
const SERVICE_NAME = 'Splitwise';
|
|
11
19
|
export class SplitwiseClient {
|
|
@@ -16,12 +24,18 @@ export class SplitwiseClient {
|
|
|
16
24
|
* Defer the config error so the server can still start (and respond to the
|
|
17
25
|
* host's install-time smoke test) when SPLITWISE_API_KEY isn't set yet.
|
|
18
26
|
* Tool calls re-raise the error at request time.
|
|
27
|
+
*
|
|
28
|
+
* Optional constructor seam: a hosted per-user deployment (e.g. a Cloudflare
|
|
29
|
+
* Worker "remote connector") builds one client per request with that user's
|
|
30
|
+
* `apiKey` injected. The stdio path passes no options, so the key resolves
|
|
31
|
+
* from the environment exactly as before — byte-for-byte identical behaviour.
|
|
19
32
|
*/
|
|
20
|
-
constructor() {
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
// Injected apiKey (hosted per-user path) takes precedence; otherwise fall
|
|
35
|
+
// back to the env var. readEnvVar trims whitespace and treats
|
|
36
|
+
// blank/`undefined`/`null`/`${...}` placeholder values as unset — defends
|
|
37
|
+
// against MCP hosts that pass .mcp.json env blocks through unexpanded.
|
|
38
|
+
const key = opts?.apiKey ?? readEnvVar('SPLITWISE_API_KEY');
|
|
25
39
|
if (!key) {
|
|
26
40
|
this.apiKey = null;
|
|
27
41
|
this.configError = new Error('SPLITWISE_API_KEY environment variable is required');
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runMcp } from '@chrischall/mcp-utils';
|
|
3
|
+
import { client } from './client.js';
|
|
3
4
|
import { registerUserTools } from './tools/user.js';
|
|
4
5
|
import { registerGroupTools } from './tools/groups.js';
|
|
5
6
|
import { registerFriendTools } from './tools/friends.js';
|
|
6
7
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
7
8
|
import { registerUtilityTools } from './tools/utilities.js';
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// runMcp builds the McpServer, applies the registrars (threading `client`
|
|
10
|
+
// through as deps), prints the banner to stderr, wires graceful shutdown, and
|
|
11
|
+
// connects the stdio transport. The Splitwise client is a module-level
|
|
12
|
+
// singleton constructed in ./client.js that defers its config error to the
|
|
13
|
+
// first request. That preserves the deferred-config-error pattern: the server
|
|
14
|
+
// boots and answers the host's install-time tools/list smoke test even when
|
|
15
|
+
// SPLITWISE_API_KEY is absent — the configuration error only surfaces on the
|
|
16
|
+
// first tool call. A hosted per-user connector (a later task) injects its own
|
|
17
|
+
// per-request client into the same registrars instead of this singleton.
|
|
13
18
|
await runMcp({
|
|
14
19
|
name: 'splitwise-mcp',
|
|
15
|
-
version: '2.0
|
|
20
|
+
version: '2.1.0', // x-release-please-version
|
|
21
|
+
deps: client,
|
|
16
22
|
banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
|
|
17
23
|
tools: [
|
|
18
24
|
registerUserTools,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SplitwiseClient } from './client.js';
|
|
2
|
+
/**
|
|
3
|
+
* `ConnectorAuth` for the Splitwise remote connector: the login page collects
|
|
4
|
+
* the user's own Splitwise API key (from https://secure.splitwise.com/apps/register),
|
|
5
|
+
* verifies it by constructing a `SplitwiseClient` with the key and calling the
|
|
6
|
+
* current-user endpoint (a bad key throws, which the connector surfaces back on
|
|
7
|
+
* the login page), and stores `{ apiKey }` as the OAuth props that `worker.ts`'s
|
|
8
|
+
* `buildClient` turns into a per-user client.
|
|
9
|
+
*/
|
|
10
|
+
export const splitwiseAuth = {
|
|
11
|
+
service: 'Splitwise',
|
|
12
|
+
accent: '#1CC29F',
|
|
13
|
+
privacyNote: 'Your Splitwise API key is stored encrypted and used only to call Splitwise on your behalf.',
|
|
14
|
+
fields: [{ name: 'apiKey', label: 'Splitwise API key', type: 'password' }],
|
|
15
|
+
async login(fields) {
|
|
16
|
+
// Verify the key up front — an invalid key makes /get_current_user throw
|
|
17
|
+
// here, which the connector surfaces back on the login page. We discard the
|
|
18
|
+
// response: the per-user client is built fresh from the stored key.
|
|
19
|
+
const client = new SplitwiseClient({ apiKey: fields.apiKey });
|
|
20
|
+
await client.request('GET', '/get_current_user');
|
|
21
|
+
return { apiKey: fields.apiKey };
|
|
22
|
+
},
|
|
23
|
+
};
|
package/dist/tools/expenses.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult, buildQueryString } from '@chrischall/mcp-utils';
|
|
3
|
-
import { client } from '../client.js';
|
|
4
3
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
5
4
|
/** Flattens a users array into Splitwise's flat-param JSON format. */
|
|
6
5
|
export function flattenUsers(users) {
|
|
@@ -32,7 +31,7 @@ const userShareSchema = z.object({
|
|
|
32
31
|
paid_share: z.string().describe('Amount this user paid, e.g. "25.00"'),
|
|
33
32
|
owed_share: z.string().describe('Amount this user owes, e.g. "12.50"'),
|
|
34
33
|
});
|
|
35
|
-
export function registerExpenseTools(server) {
|
|
34
|
+
export function registerExpenseTools(server, client) {
|
|
36
35
|
server.registerTool('sw_list_expenses', {
|
|
37
36
|
description: 'List or search Splitwise expenses. All filters are optional. Use group_id to filter by group, dated_after/dated_before for date ranges.',
|
|
38
37
|
annotations: { readOnlyHint: true },
|
package/dist/tools/friends.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
-
import { client } from '../client.js';
|
|
4
3
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
5
|
-
export function registerFriendTools(server) {
|
|
4
|
+
export function registerFriendTools(server, client) {
|
|
6
5
|
server.registerTool('sw_list_friends', {
|
|
7
6
|
description: "List all Splitwise friends with their id, first_name, last_name, and email. Use this to resolve a friend's name to a user_id before adding them to a group or building a custom expense split.",
|
|
8
7
|
annotations: { readOnlyHint: true },
|
package/dist/tools/groups.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
-
import { client } from '../client.js';
|
|
4
3
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
5
|
-
export function registerGroupTools(server) {
|
|
4
|
+
export function registerGroupTools(server, client) {
|
|
6
5
|
server.registerTool('sw_list_groups', {
|
|
7
6
|
description: 'List all Splitwise groups the current user belongs to. Returns id, name, and members for each group. Use this to resolve a group name to its id.',
|
|
8
7
|
annotations: { readOnlyHint: true },
|
package/dist/tools/user.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
-
import { client } from '../client.js';
|
|
4
3
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
5
|
-
export function registerUserTools(server) {
|
|
4
|
+
export function registerUserTools(server, client) {
|
|
6
5
|
server.registerTool('sw_get_current_user', {
|
|
7
6
|
description: "Get the authenticated Splitwise user's profile (id, first_name, last_name, email). Use the returned id when building custom expense splits.",
|
|
8
7
|
annotations: { readOnlyHint: true },
|
package/dist/tools/utilities.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult, buildQueryString } from '@chrischall/mcp-utils';
|
|
3
|
-
import { client } from '../client.js';
|
|
4
3
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
5
|
-
export function registerUtilityTools(server) {
|
|
4
|
+
export function registerUtilityTools(server, client) {
|
|
6
5
|
server.registerTool('sw_get_notifications', {
|
|
7
6
|
description: 'Get recent Splitwise activity notifications for the current user.',
|
|
8
7
|
annotations: { readOnlyHint: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splitwise-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/splitwise-mcp",
|
|
5
5
|
"description": "Splitwise MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"dev": "node dist/index.js",
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"test:watch": "vitest",
|
|
43
|
-
"test:coverage": "vitest run --coverage"
|
|
43
|
+
"test:coverage": "vitest run --coverage",
|
|
44
|
+
"worker:dev": "wrangler dev",
|
|
45
|
+
"worker:deploy": "wrangler deploy",
|
|
46
|
+
"worker:test": "vitest run --config vitest.workers.config.ts"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
49
|
"@chrischall/mcp-utils": "^0.13.0",
|
|
@@ -49,10 +52,16 @@
|
|
|
49
52
|
"zod": "^4.4.2"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
55
|
+
"@chrischall/mcp-connector": "^0.1.0",
|
|
56
|
+
"@cloudflare/vitest-pool-workers": "^0.18.4",
|
|
57
|
+
"@cloudflare/workers-oauth-provider": "^0.0.11",
|
|
58
|
+
"@cloudflare/workers-types": "^5.20260708.1",
|
|
52
59
|
"@types/node": "^26.0.0",
|
|
53
60
|
"@vitest/coverage-v8": "^4.1.2",
|
|
61
|
+
"agents": "^0.17.3",
|
|
54
62
|
"esbuild": "^0.28.0",
|
|
55
63
|
"typescript": "^7.0.2",
|
|
56
|
-
"vitest": "^4.1.2"
|
|
64
|
+
"vitest": "^4.1.2",
|
|
65
|
+
"wrangler": "^4.110.0"
|
|
57
66
|
}
|
|
58
67
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/splitwise-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.0
|
|
9
|
+
"version": "2.1.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "splitwise-mcp",
|
|
14
|
-
"version": "2.0
|
|
14
|
+
"version": "2.1.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|