splitwise-mcp 2.0.10 → 2.0.12
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 +2 -2
- package/dist/bundle.js +129 -36
- package/dist/index.js +1 -1
- package/dist/tools/_confirm.js +21 -0
- package/dist/tools/expenses.js +19 -5
- package/dist/tools/friends.js +14 -4
- package/dist/tools/groups.js +25 -6
- package/dist/tools/user.js +8 -1
- package/dist/tools/utilities.js +14 -4
- package/package.json +4 -4
- package/server.json +2 -2
- /package/{SKILL.md → skills/splitwise-mcp/SKILL.md} +0 -0
|
@@ -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.0.12"
|
|
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.0.12",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splitwise-mcp",
|
|
3
3
|
"displayName": "Splitwise",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.12",
|
|
5
5
|
"description": "MCP server for Splitwise — natural-language expense and group management via the Splitwise API",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
"mcp",
|
|
17
17
|
"finance"
|
|
18
18
|
],
|
|
19
|
-
"skills": "./
|
|
19
|
+
"skills": "./skills/",
|
|
20
20
|
"mcp": "./.mcp.json"
|
|
21
21
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -31021,8 +31021,12 @@ var API_KEY_RE = new RegExp([
|
|
|
31021
31021
|
// webhook signing secret (Stripe-style)
|
|
31022
31022
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
31023
31023
|
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
31024
|
+
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
31025
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
31026
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
31027
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
31024
31028
|
function redactSecrets(text) {
|
|
31025
|
-
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
31029
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
31026
31030
|
}
|
|
31027
31031
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
31028
31032
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
@@ -31069,6 +31073,18 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
31069
31073
|
}
|
|
31070
31074
|
}
|
|
31071
31075
|
|
|
31076
|
+
// node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js
|
|
31077
|
+
function parseRetryAfterMs(header, opts = {}) {
|
|
31078
|
+
const defaultMs = opts.defaultMs ?? 2e3;
|
|
31079
|
+
const capMs = opts.capMs ?? 3e4;
|
|
31080
|
+
if (header == null)
|
|
31081
|
+
return defaultMs;
|
|
31082
|
+
const trimmed = header.trim();
|
|
31083
|
+
if (!/^\d+$/.test(trimmed))
|
|
31084
|
+
return defaultMs;
|
|
31085
|
+
return Math.min(Number(trimmed) * 1e3, capMs);
|
|
31086
|
+
}
|
|
31087
|
+
|
|
31072
31088
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
31073
31089
|
var DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
31074
31090
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -31160,7 +31176,11 @@ function createApiClient(opts) {
|
|
|
31160
31176
|
const res = await once();
|
|
31161
31177
|
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
31162
31178
|
attempt += 1;
|
|
31163
|
-
|
|
31179
|
+
const delay = retry.honorRetryAfter ? parseRetryAfterMs(res.headers.get("retry-after"), {
|
|
31180
|
+
defaultMs: retry.delayMs,
|
|
31181
|
+
capMs: retry.maxRetryAfterMs ?? 3e4
|
|
31182
|
+
}) : retry.delayMs;
|
|
31183
|
+
await sleep(delay);
|
|
31164
31184
|
continue;
|
|
31165
31185
|
}
|
|
31166
31186
|
return res;
|
|
@@ -31195,7 +31215,26 @@ function createApiClient(opts) {
|
|
|
31195
31215
|
}
|
|
31196
31216
|
return text;
|
|
31197
31217
|
}
|
|
31198
|
-
|
|
31218
|
+
async function fetchRaw(method, path, opt = {}) {
|
|
31219
|
+
const headers = { Accept: "*/*", ...opt.headers };
|
|
31220
|
+
const res = await send(method, path, { ...opt, headers });
|
|
31221
|
+
if (res.status === 401)
|
|
31222
|
+
throw unauthorized();
|
|
31223
|
+
if (res.status === 429)
|
|
31224
|
+
throw rateLimited();
|
|
31225
|
+
if (!res.ok) {
|
|
31226
|
+
const text = await res.text().catch(() => "");
|
|
31227
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
31228
|
+
}
|
|
31229
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
31230
|
+
return {
|
|
31231
|
+
status: res.status,
|
|
31232
|
+
contentType: res.headers.get("content-type"),
|
|
31233
|
+
headers: res.headers,
|
|
31234
|
+
bytes
|
|
31235
|
+
};
|
|
31236
|
+
}
|
|
31237
|
+
return { fetchJson, fetchHtml, fetchRaw };
|
|
31199
31238
|
}
|
|
31200
31239
|
function buildQueryString(params) {
|
|
31201
31240
|
const parts = [];
|
|
@@ -31285,6 +31324,19 @@ var SplitwiseClient = class {
|
|
|
31285
31324
|
};
|
|
31286
31325
|
var client = new SplitwiseClient();
|
|
31287
31326
|
|
|
31327
|
+
// src/tools/_confirm.ts
|
|
31328
|
+
function previewUnlessConfirmed(confirm, action, method, path, body) {
|
|
31329
|
+
if (confirm === true) return null;
|
|
31330
|
+
return textResult({
|
|
31331
|
+
dryRun: true,
|
|
31332
|
+
action,
|
|
31333
|
+
method,
|
|
31334
|
+
path,
|
|
31335
|
+
...body !== void 0 ? { willSend: body } : {},
|
|
31336
|
+
note: "Re-run with confirm: true to execute."
|
|
31337
|
+
});
|
|
31338
|
+
}
|
|
31339
|
+
|
|
31288
31340
|
// src/tools/user.ts
|
|
31289
31341
|
function registerUserTools(server) {
|
|
31290
31342
|
server.registerTool("sw_get_current_user", {
|
|
@@ -31313,9 +31365,10 @@ function registerUserTools(server) {
|
|
|
31313
31365
|
email: external_exports.string().optional(),
|
|
31314
31366
|
password: external_exports.string().optional(),
|
|
31315
31367
|
locale: external_exports.string().optional(),
|
|
31316
|
-
default_currency: external_exports.string().optional()
|
|
31368
|
+
default_currency: external_exports.string().optional(),
|
|
31369
|
+
confirm: schemaConfirm
|
|
31317
31370
|
}
|
|
31318
|
-
}, async ({ id, first_name, last_name, email: email3, password, locale, default_currency }) => {
|
|
31371
|
+
}, async ({ id, first_name, last_name, email: email3, password, locale, default_currency, confirm }) => {
|
|
31319
31372
|
const body = {};
|
|
31320
31373
|
if (first_name !== void 0) body.first_name = first_name;
|
|
31321
31374
|
if (last_name !== void 0) body.last_name = last_name;
|
|
@@ -31323,6 +31376,9 @@ function registerUserTools(server) {
|
|
|
31323
31376
|
if (password !== void 0) body.password = password;
|
|
31324
31377
|
if (locale !== void 0) body.locale = locale;
|
|
31325
31378
|
if (default_currency !== void 0) body.default_currency = default_currency;
|
|
31379
|
+
const previewBody = { ...body, ...password !== void 0 ? { password: "[hidden]" } : {} };
|
|
31380
|
+
const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, "POST", `/update_user/${id}`, previewBody);
|
|
31381
|
+
if (gate) return gate;
|
|
31326
31382
|
const data = await client.request("POST", `/update_user/${id}`, body);
|
|
31327
31383
|
return textResult(data);
|
|
31328
31384
|
});
|
|
@@ -31349,15 +31405,19 @@ function registerGroupTools(server) {
|
|
|
31349
31405
|
});
|
|
31350
31406
|
server.registerTool("sw_create_group", {
|
|
31351
31407
|
description: "Create a new Splitwise group.",
|
|
31408
|
+
annotations: { destructiveHint: true },
|
|
31352
31409
|
inputSchema: {
|
|
31353
31410
|
name: external_exports.string().describe("Group name"),
|
|
31354
31411
|
group_type: external_exports.enum(["apartment", "house", "trip", "other"]).describe("Type of group").optional(),
|
|
31355
|
-
simplify_by_default: external_exports.boolean().describe("Whether to simplify debts by default").optional()
|
|
31412
|
+
simplify_by_default: external_exports.boolean().describe("Whether to simplify debts by default").optional(),
|
|
31413
|
+
confirm: schemaConfirm
|
|
31356
31414
|
}
|
|
31357
|
-
}, async ({ name, group_type, simplify_by_default }) => {
|
|
31415
|
+
}, async ({ name, group_type, simplify_by_default, confirm }) => {
|
|
31358
31416
|
const body = { name };
|
|
31359
31417
|
if (group_type !== void 0) body.group_type = group_type;
|
|
31360
31418
|
if (simplify_by_default !== void 0) body.simplify_by_default = simplify_by_default;
|
|
31419
|
+
const gate = previewUnlessConfirmed(confirm, `Create Splitwise group "${name}"`, "POST", "/create_group", body);
|
|
31420
|
+
if (gate) return gate;
|
|
31361
31421
|
const data = await client.request("POST", "/create_group", body);
|
|
31362
31422
|
return textResult(data);
|
|
31363
31423
|
});
|
|
@@ -31368,9 +31428,10 @@ function registerGroupTools(server) {
|
|
|
31368
31428
|
user_id: external_exports.number().describe("User ID (preferred)").optional(),
|
|
31369
31429
|
first_name: external_exports.string().optional(),
|
|
31370
31430
|
last_name: external_exports.string().optional(),
|
|
31371
|
-
email: external_exports.string().optional()
|
|
31431
|
+
email: external_exports.string().optional(),
|
|
31432
|
+
confirm: schemaConfirm
|
|
31372
31433
|
}
|
|
31373
|
-
}, async ({ group_id, user_id, first_name, last_name, email: email3 }) => {
|
|
31434
|
+
}, async ({ group_id, user_id, first_name, last_name, email: email3, confirm }) => {
|
|
31374
31435
|
let body;
|
|
31375
31436
|
if (user_id !== void 0) {
|
|
31376
31437
|
body = { group_id, user_id };
|
|
@@ -31380,26 +31441,35 @@ function registerGroupTools(server) {
|
|
|
31380
31441
|
}
|
|
31381
31442
|
body = { group_id, first_name, last_name, email: email3 };
|
|
31382
31443
|
}
|
|
31444
|
+
const gate = previewUnlessConfirmed(confirm, `Add a user to Splitwise group ${group_id} (may send an invite email)`, "POST", "/add_user_to_group", body);
|
|
31445
|
+
if (gate) return gate;
|
|
31383
31446
|
const data = await client.request("POST", "/add_user_to_group", body);
|
|
31384
31447
|
return textResult(data);
|
|
31385
31448
|
});
|
|
31386
31449
|
server.registerTool("sw_remove_user_from_group", {
|
|
31387
|
-
description: "Remove a user from a Splitwise group.",
|
|
31450
|
+
description: "Remove a user from a Splitwise group. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it removes the user.",
|
|
31451
|
+
annotations: { destructiveHint: true },
|
|
31388
31452
|
inputSchema: {
|
|
31389
31453
|
group_id: external_exports.number().describe("Group ID"),
|
|
31390
|
-
user_id: external_exports.number().describe("User ID to remove")
|
|
31454
|
+
user_id: external_exports.number().describe("User ID to remove"),
|
|
31455
|
+
confirm: schemaConfirm
|
|
31391
31456
|
}
|
|
31392
|
-
}, async ({ group_id, user_id }) => {
|
|
31457
|
+
}, async ({ group_id, user_id, confirm }) => {
|
|
31458
|
+
const gate = previewUnlessConfirmed(confirm, `Remove user ${user_id} from Splitwise group ${group_id}`, "POST", "/remove_user_from_group", { group_id, user_id });
|
|
31459
|
+
if (gate) return gate;
|
|
31393
31460
|
const data = await client.request("POST", "/remove_user_from_group", { group_id, user_id });
|
|
31394
31461
|
return textResult(data);
|
|
31395
31462
|
});
|
|
31396
31463
|
server.registerTool("sw_delete_group", {
|
|
31397
|
-
description: "Soft-delete a Splitwise group.",
|
|
31464
|
+
description: "Soft-delete a Splitwise group. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.",
|
|
31398
31465
|
annotations: { destructiveHint: true },
|
|
31399
31466
|
inputSchema: {
|
|
31400
|
-
id: external_exports.number().describe("Group ID to delete")
|
|
31467
|
+
id: external_exports.number().describe("Group ID to delete"),
|
|
31468
|
+
confirm: schemaConfirm
|
|
31401
31469
|
}
|
|
31402
|
-
}, async ({ id }) => {
|
|
31470
|
+
}, async ({ id, confirm }) => {
|
|
31471
|
+
const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise group ${id}`, "POST", `/delete_group/${id}`);
|
|
31472
|
+
if (gate) return gate;
|
|
31403
31473
|
const data = await client.request("POST", `/delete_group/${id}`);
|
|
31404
31474
|
return textResult(data);
|
|
31405
31475
|
});
|
|
@@ -31424,26 +31494,33 @@ function registerFriendTools(server) {
|
|
|
31424
31494
|
return textResult(data);
|
|
31425
31495
|
});
|
|
31426
31496
|
server.registerTool("sw_create_friend", {
|
|
31427
|
-
description: "Add a Splitwise friend by email.",
|
|
31497
|
+
description: "Add a Splitwise friend by email (sends them an invite). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it adds the friend.",
|
|
31498
|
+
annotations: { destructiveHint: true },
|
|
31428
31499
|
inputSchema: {
|
|
31429
31500
|
user_email: external_exports.string().describe("Email of the user to add as a friend"),
|
|
31430
31501
|
user_first_name: external_exports.string().describe("First name of the user").optional(),
|
|
31431
|
-
user_last_name: external_exports.string().describe("Last name of the user").optional()
|
|
31502
|
+
user_last_name: external_exports.string().describe("Last name of the user").optional(),
|
|
31503
|
+
confirm: schemaConfirm
|
|
31432
31504
|
}
|
|
31433
|
-
}, async ({ user_email, user_first_name, user_last_name }) => {
|
|
31505
|
+
}, async ({ user_email, user_first_name, user_last_name, confirm }) => {
|
|
31434
31506
|
const body = { user_email };
|
|
31435
31507
|
if (user_first_name !== void 0) body.user_first_name = user_first_name;
|
|
31436
31508
|
if (user_last_name !== void 0) body.user_last_name = user_last_name;
|
|
31509
|
+
const gate = previewUnlessConfirmed(confirm, `Add ${user_email} as a Splitwise friend`, "POST", "/create_friend", body);
|
|
31510
|
+
if (gate) return gate;
|
|
31437
31511
|
const data = await client.request("POST", "/create_friend", body);
|
|
31438
31512
|
return textResult(data);
|
|
31439
31513
|
});
|
|
31440
31514
|
server.registerTool("sw_delete_friend", {
|
|
31441
|
-
description: "Remove a Splitwise friendship by user id.",
|
|
31515
|
+
description: "Remove a Splitwise friendship by user id. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it removes the friendship.",
|
|
31442
31516
|
annotations: { destructiveHint: true },
|
|
31443
31517
|
inputSchema: {
|
|
31444
|
-
id: external_exports.number().describe("User ID of the friend to remove")
|
|
31518
|
+
id: external_exports.number().describe("User ID of the friend to remove"),
|
|
31519
|
+
confirm: schemaConfirm
|
|
31445
31520
|
}
|
|
31446
|
-
}, async ({ id }) => {
|
|
31521
|
+
}, async ({ id, confirm }) => {
|
|
31522
|
+
const gate = previewUnlessConfirmed(confirm, `Remove Splitwise friendship with user ${id}`, "POST", `/delete_friend/${id}`);
|
|
31523
|
+
if (gate) return gate;
|
|
31447
31524
|
const data = await client.request("POST", `/delete_friend/${id}`);
|
|
31448
31525
|
return textResult(data);
|
|
31449
31526
|
});
|
|
@@ -31460,7 +31537,7 @@ function flattenUsers(users) {
|
|
|
31460
31537
|
return flat;
|
|
31461
31538
|
}
|
|
31462
31539
|
function buildExpenseBody(args) {
|
|
31463
|
-
const { split_equally, users, expense_id: _id, ...rest } = args;
|
|
31540
|
+
const { split_equally, users, expense_id: _id, confirm: _confirm, ...rest } = args;
|
|
31464
31541
|
if (split_equally && users) {
|
|
31465
31542
|
throw new Error("Provide either split_equally or users, not both");
|
|
31466
31543
|
}
|
|
@@ -31516,7 +31593,7 @@ function registerExpenseTools(server) {
|
|
|
31516
31593
|
return textResult(data);
|
|
31517
31594
|
});
|
|
31518
31595
|
server.registerTool("sw_create_expense", {
|
|
31519
|
-
description: 'Create a Splitwise expense. Use split_equally:true to split evenly among group members, or provide a users array for custom per-person splits (paid_share and owed_share as decimal strings like "25.00"). cost must be a decimal string.',
|
|
31596
|
+
description: 'Create a Splitwise expense. Use split_equally:true to split evenly among group members, or provide a users array for custom per-person splits (paid_share and owed_share as decimal strings like "25.00"). cost must be a decimal string. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it creates the expense.',
|
|
31520
31597
|
inputSchema: {
|
|
31521
31598
|
group_id: external_exports.number().describe("Group to add expense to (use 0 for no group)"),
|
|
31522
31599
|
description: external_exports.string().describe("Short description of the expense"),
|
|
@@ -31526,15 +31603,18 @@ function registerExpenseTools(server) {
|
|
|
31526
31603
|
currency_code: external_exports.string().describe('Currency code, e.g. "USD". Defaults to group/user default.').optional(),
|
|
31527
31604
|
date: external_exports.string().describe("ISO 8601 datetime").optional(),
|
|
31528
31605
|
category_id: external_exports.number().describe("Category id from sw_get_categories").optional(),
|
|
31529
|
-
details: external_exports.string().describe("Notes").optional()
|
|
31606
|
+
details: external_exports.string().describe("Notes").optional(),
|
|
31607
|
+
confirm: schemaConfirm
|
|
31530
31608
|
}
|
|
31531
31609
|
}, async (args) => {
|
|
31532
31610
|
const body = buildExpenseBody(args);
|
|
31611
|
+
const gate = previewUnlessConfirmed(args.confirm, `Create a Splitwise expense "${args.description}" (${args.cost}) \u2014 notifies group members`, "POST", "/create_expense", body);
|
|
31612
|
+
if (gate) return gate;
|
|
31533
31613
|
const data = await client.request("POST", "/create_expense", body);
|
|
31534
31614
|
return textResult(data);
|
|
31535
31615
|
});
|
|
31536
31616
|
server.registerTool("sw_update_expense", {
|
|
31537
|
-
description: "Edit an existing Splitwise expense. Provide expense_id and any fields to change. For custom split updates, the full users array must be provided (the API replaces the entire split).",
|
|
31617
|
+
description: "Edit an existing Splitwise expense. Provide expense_id and any fields to change. For custom split updates, the full users array must be provided (the API replaces the entire split). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it updates the expense.",
|
|
31538
31618
|
inputSchema: {
|
|
31539
31619
|
expense_id: external_exports.number().describe("ID of the expense to update"),
|
|
31540
31620
|
description: external_exports.string().optional(),
|
|
@@ -31544,21 +31624,27 @@ function registerExpenseTools(server) {
|
|
|
31544
31624
|
currency_code: external_exports.string().optional(),
|
|
31545
31625
|
date: external_exports.string().optional(),
|
|
31546
31626
|
category_id: external_exports.number().optional(),
|
|
31547
|
-
details: external_exports.string().optional()
|
|
31627
|
+
details: external_exports.string().optional(),
|
|
31628
|
+
confirm: schemaConfirm
|
|
31548
31629
|
}
|
|
31549
31630
|
}, async (args) => {
|
|
31550
31631
|
const { expense_id } = args;
|
|
31551
31632
|
const body = buildExpenseBody(args);
|
|
31633
|
+
const gate = previewUnlessConfirmed(args.confirm, `Update Splitwise expense ${expense_id} \u2014 notifies group members`, "POST", `/update_expense/${expense_id}`, body);
|
|
31634
|
+
if (gate) return gate;
|
|
31552
31635
|
const data = await client.request("POST", `/update_expense/${expense_id}`, body);
|
|
31553
31636
|
return textResult(data);
|
|
31554
31637
|
});
|
|
31555
31638
|
server.registerTool("sw_delete_expense", {
|
|
31556
|
-
description: "Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore.",
|
|
31639
|
+
description: "Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.",
|
|
31557
31640
|
annotations: { destructiveHint: true },
|
|
31558
31641
|
inputSchema: {
|
|
31559
|
-
id: external_exports.number().describe("Expense ID to delete")
|
|
31642
|
+
id: external_exports.number().describe("Expense ID to delete"),
|
|
31643
|
+
confirm: schemaConfirm
|
|
31560
31644
|
}
|
|
31561
|
-
}, async ({ id }) => {
|
|
31645
|
+
}, async ({ id, confirm }) => {
|
|
31646
|
+
const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise expense ${id}`, "POST", `/delete_expense/${id}`);
|
|
31647
|
+
if (gate) return gate;
|
|
31562
31648
|
const data = await client.request("POST", `/delete_expense/${id}`);
|
|
31563
31649
|
return textResult(data);
|
|
31564
31650
|
});
|
|
@@ -31607,22 +31693,29 @@ function registerUtilityTools(server) {
|
|
|
31607
31693
|
return textResult(data);
|
|
31608
31694
|
});
|
|
31609
31695
|
server.registerTool("sw_create_comment", {
|
|
31610
|
-
description: "Add a comment to a Splitwise expense.",
|
|
31696
|
+
description: "Add a comment to a Splitwise expense (visible to other participants). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it posts.",
|
|
31697
|
+
annotations: { destructiveHint: true },
|
|
31611
31698
|
inputSchema: {
|
|
31612
31699
|
expense_id: external_exports.number().describe("Expense ID to comment on"),
|
|
31613
|
-
content: external_exports.string().describe("Comment text")
|
|
31700
|
+
content: external_exports.string().describe("Comment text"),
|
|
31701
|
+
confirm: schemaConfirm
|
|
31614
31702
|
}
|
|
31615
|
-
}, async ({ expense_id, content }) => {
|
|
31703
|
+
}, async ({ expense_id, content, confirm }) => {
|
|
31704
|
+
const gate = previewUnlessConfirmed(confirm, `Comment on Splitwise expense ${expense_id} (visible to participants)`, "POST", "/create_comment", { expense_id, content });
|
|
31705
|
+
if (gate) return gate;
|
|
31616
31706
|
const data = await client.request("POST", "/create_comment", { expense_id, content });
|
|
31617
31707
|
return textResult(data);
|
|
31618
31708
|
});
|
|
31619
31709
|
server.registerTool("sw_delete_comment", {
|
|
31620
|
-
description: "Delete a comment by id.",
|
|
31710
|
+
description: "Delete a comment by id. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.",
|
|
31621
31711
|
annotations: { destructiveHint: true },
|
|
31622
31712
|
inputSchema: {
|
|
31623
|
-
id: external_exports.number().describe("Comment ID to delete")
|
|
31713
|
+
id: external_exports.number().describe("Comment ID to delete"),
|
|
31714
|
+
confirm: schemaConfirm
|
|
31624
31715
|
}
|
|
31625
|
-
}, async ({ id }) => {
|
|
31716
|
+
}, async ({ id, confirm }) => {
|
|
31717
|
+
const gate = previewUnlessConfirmed(confirm, `Delete Splitwise comment ${id}`, "POST", `/delete_comment/${id}`);
|
|
31718
|
+
if (gate) return gate;
|
|
31626
31719
|
const data = await client.request("POST", `/delete_comment/${id}`);
|
|
31627
31720
|
return textResult(data);
|
|
31628
31721
|
});
|
|
@@ -31631,7 +31724,7 @@ function registerUtilityTools(server) {
|
|
|
31631
31724
|
// src/index.ts
|
|
31632
31725
|
await runMcp({
|
|
31633
31726
|
name: "splitwise-mcp",
|
|
31634
|
-
version: "2.0.
|
|
31727
|
+
version: "2.0.12",
|
|
31635
31728
|
// x-release-please-version
|
|
31636
31729
|
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
|
31637
31730
|
tools: [
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerUtilityTools } from './tools/utilities.js';
|
|
|
12
12
|
// the configuration error only surfaces on the first tool call.
|
|
13
13
|
await runMcp({
|
|
14
14
|
name: 'splitwise-mcp',
|
|
15
|
-
version: '2.0.
|
|
15
|
+
version: '2.0.12', // x-release-please-version
|
|
16
16
|
banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
|
|
17
17
|
tools: [
|
|
18
18
|
registerUserTools,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { schemaConfirm, textResult } from '@chrischall/mcp-utils';
|
|
2
|
+
export { schemaConfirm };
|
|
3
|
+
/**
|
|
4
|
+
* Confirm-gate for a mutating tool (the fleet convention). When `confirm` is not
|
|
5
|
+
* `true`, returns a no-network dry-run preview of exactly what would be sent;
|
|
6
|
+
* when it is `true`, returns `null` so the caller proceeds with the write.
|
|
7
|
+
* Splitwise writes notify other people (friends / group members), so a single
|
|
8
|
+
* hallucinated call shouldn't fire silently.
|
|
9
|
+
*/
|
|
10
|
+
export function previewUnlessConfirmed(confirm, action, method, path, body) {
|
|
11
|
+
if (confirm === true)
|
|
12
|
+
return null;
|
|
13
|
+
return textResult({
|
|
14
|
+
dryRun: true,
|
|
15
|
+
action,
|
|
16
|
+
method,
|
|
17
|
+
path,
|
|
18
|
+
...(body !== undefined ? { willSend: body } : {}),
|
|
19
|
+
note: 'Re-run with confirm: true to execute.',
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/tools/expenses.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult, buildQueryString } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from '../client.js';
|
|
4
|
+
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
4
5
|
/** Flattens a users array into Splitwise's flat-param JSON format. */
|
|
5
6
|
export function flattenUsers(users) {
|
|
6
7
|
const flat = {};
|
|
@@ -12,7 +13,8 @@ export function flattenUsers(users) {
|
|
|
12
13
|
return flat;
|
|
13
14
|
}
|
|
14
15
|
function buildExpenseBody(args) {
|
|
15
|
-
|
|
16
|
+
// `confirm` is a tool-gate flag, not part of the Splitwise expense payload.
|
|
17
|
+
const { split_equally, users, expense_id: _id, confirm: _confirm, ...rest } = args;
|
|
16
18
|
if (split_equally && users) {
|
|
17
19
|
throw new Error('Provide either split_equally or users, not both');
|
|
18
20
|
}
|
|
@@ -69,7 +71,7 @@ export function registerExpenseTools(server) {
|
|
|
69
71
|
return textResult(data);
|
|
70
72
|
});
|
|
71
73
|
server.registerTool('sw_create_expense', {
|
|
72
|
-
description: 'Create a Splitwise expense. Use split_equally:true to split evenly among group members, or provide a users array for custom per-person splits (paid_share and owed_share as decimal strings like "25.00"). cost must be a decimal string.',
|
|
74
|
+
description: 'Create a Splitwise expense. Use split_equally:true to split evenly among group members, or provide a users array for custom per-person splits (paid_share and owed_share as decimal strings like "25.00"). cost must be a decimal string. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it creates the expense.',
|
|
73
75
|
inputSchema: {
|
|
74
76
|
group_id: z.number().describe('Group to add expense to (use 0 for no group)'),
|
|
75
77
|
description: z.string().describe('Short description of the expense'),
|
|
@@ -80,14 +82,18 @@ export function registerExpenseTools(server) {
|
|
|
80
82
|
date: z.string().describe('ISO 8601 datetime').optional(),
|
|
81
83
|
category_id: z.number().describe('Category id from sw_get_categories').optional(),
|
|
82
84
|
details: z.string().describe('Notes').optional(),
|
|
85
|
+
confirm: schemaConfirm,
|
|
83
86
|
},
|
|
84
87
|
}, async (args) => {
|
|
85
88
|
const body = buildExpenseBody(args);
|
|
89
|
+
const gate = previewUnlessConfirmed(args.confirm, `Create a Splitwise expense "${args.description}" (${args.cost}) — notifies group members`, 'POST', '/create_expense', body);
|
|
90
|
+
if (gate)
|
|
91
|
+
return gate;
|
|
86
92
|
const data = await client.request('POST', '/create_expense', body);
|
|
87
93
|
return textResult(data);
|
|
88
94
|
});
|
|
89
95
|
server.registerTool('sw_update_expense', {
|
|
90
|
-
description: 'Edit an existing Splitwise expense. Provide expense_id and any fields to change. For custom split updates, the full users array must be provided (the API replaces the entire split).',
|
|
96
|
+
description: 'Edit an existing Splitwise expense. Provide expense_id and any fields to change. For custom split updates, the full users array must be provided (the API replaces the entire split). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it updates the expense.',
|
|
91
97
|
inputSchema: {
|
|
92
98
|
expense_id: z.number().describe('ID of the expense to update'),
|
|
93
99
|
description: z.string().optional(),
|
|
@@ -98,20 +104,28 @@ export function registerExpenseTools(server) {
|
|
|
98
104
|
date: z.string().optional(),
|
|
99
105
|
category_id: z.number().optional(),
|
|
100
106
|
details: z.string().optional(),
|
|
107
|
+
confirm: schemaConfirm,
|
|
101
108
|
},
|
|
102
109
|
}, async (args) => {
|
|
103
110
|
const { expense_id } = args;
|
|
104
111
|
const body = buildExpenseBody(args);
|
|
112
|
+
const gate = previewUnlessConfirmed(args.confirm, `Update Splitwise expense ${expense_id} — notifies group members`, 'POST', `/update_expense/${expense_id}`, body);
|
|
113
|
+
if (gate)
|
|
114
|
+
return gate;
|
|
105
115
|
const data = await client.request('POST', `/update_expense/${expense_id}`, body);
|
|
106
116
|
return textResult(data);
|
|
107
117
|
});
|
|
108
118
|
server.registerTool('sw_delete_expense', {
|
|
109
|
-
description: 'Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore.',
|
|
119
|
+
description: 'Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.',
|
|
110
120
|
annotations: { destructiveHint: true },
|
|
111
121
|
inputSchema: {
|
|
112
122
|
id: z.number().describe('Expense ID to delete'),
|
|
123
|
+
confirm: schemaConfirm,
|
|
113
124
|
},
|
|
114
|
-
}, async ({ id }) => {
|
|
125
|
+
}, async ({ id, confirm }) => {
|
|
126
|
+
const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise expense ${id}`, 'POST', `/delete_expense/${id}`);
|
|
127
|
+
if (gate)
|
|
128
|
+
return gate;
|
|
115
129
|
const data = await client.request('POST', `/delete_expense/${id}`);
|
|
116
130
|
return textResult(data);
|
|
117
131
|
});
|
package/dist/tools/friends.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from '../client.js';
|
|
4
|
+
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
4
5
|
export function registerFriendTools(server) {
|
|
5
6
|
server.registerTool('sw_list_friends', {
|
|
6
7
|
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.",
|
|
@@ -10,28 +11,37 @@ export function registerFriendTools(server) {
|
|
|
10
11
|
return textResult(data);
|
|
11
12
|
});
|
|
12
13
|
server.registerTool('sw_create_friend', {
|
|
13
|
-
description: 'Add a Splitwise friend by email.',
|
|
14
|
+
description: 'Add a Splitwise friend by email (sends them an invite). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it adds the friend.',
|
|
15
|
+
annotations: { destructiveHint: true },
|
|
14
16
|
inputSchema: {
|
|
15
17
|
user_email: z.string().describe('Email of the user to add as a friend'),
|
|
16
18
|
user_first_name: z.string().describe('First name of the user').optional(),
|
|
17
19
|
user_last_name: z.string().describe('Last name of the user').optional(),
|
|
20
|
+
confirm: schemaConfirm,
|
|
18
21
|
},
|
|
19
|
-
}, async ({ user_email, user_first_name, user_last_name }) => {
|
|
22
|
+
}, async ({ user_email, user_first_name, user_last_name, confirm }) => {
|
|
20
23
|
const body = { user_email };
|
|
21
24
|
if (user_first_name !== undefined)
|
|
22
25
|
body.user_first_name = user_first_name;
|
|
23
26
|
if (user_last_name !== undefined)
|
|
24
27
|
body.user_last_name = user_last_name;
|
|
28
|
+
const gate = previewUnlessConfirmed(confirm, `Add ${user_email} as a Splitwise friend`, 'POST', '/create_friend', body);
|
|
29
|
+
if (gate)
|
|
30
|
+
return gate;
|
|
25
31
|
const data = await client.request('POST', '/create_friend', body);
|
|
26
32
|
return textResult(data);
|
|
27
33
|
});
|
|
28
34
|
server.registerTool('sw_delete_friend', {
|
|
29
|
-
description: 'Remove a Splitwise friendship by user id.',
|
|
35
|
+
description: 'Remove a Splitwise friendship by user id. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it removes the friendship.',
|
|
30
36
|
annotations: { destructiveHint: true },
|
|
31
37
|
inputSchema: {
|
|
32
38
|
id: z.number().describe('User ID of the friend to remove'),
|
|
39
|
+
confirm: schemaConfirm,
|
|
33
40
|
},
|
|
34
|
-
}, async ({ id }) => {
|
|
41
|
+
}, async ({ id, confirm }) => {
|
|
42
|
+
const gate = previewUnlessConfirmed(confirm, `Remove Splitwise friendship with user ${id}`, 'POST', `/delete_friend/${id}`);
|
|
43
|
+
if (gate)
|
|
44
|
+
return gate;
|
|
35
45
|
const data = await client.request('POST', `/delete_friend/${id}`);
|
|
36
46
|
return textResult(data);
|
|
37
47
|
});
|
package/dist/tools/groups.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from '../client.js';
|
|
4
|
+
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
4
5
|
export function registerGroupTools(server) {
|
|
5
6
|
server.registerTool('sw_list_groups', {
|
|
6
7
|
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.',
|
|
@@ -21,17 +22,22 @@ export function registerGroupTools(server) {
|
|
|
21
22
|
});
|
|
22
23
|
server.registerTool('sw_create_group', {
|
|
23
24
|
description: 'Create a new Splitwise group.',
|
|
25
|
+
annotations: { destructiveHint: true },
|
|
24
26
|
inputSchema: {
|
|
25
27
|
name: z.string().describe('Group name'),
|
|
26
28
|
group_type: z.enum(['apartment', 'house', 'trip', 'other']).describe('Type of group').optional(),
|
|
27
29
|
simplify_by_default: z.boolean().describe('Whether to simplify debts by default').optional(),
|
|
30
|
+
confirm: schemaConfirm,
|
|
28
31
|
},
|
|
29
|
-
}, async ({ name, group_type, simplify_by_default }) => {
|
|
32
|
+
}, async ({ name, group_type, simplify_by_default, confirm }) => {
|
|
30
33
|
const body = { name };
|
|
31
34
|
if (group_type !== undefined)
|
|
32
35
|
body.group_type = group_type;
|
|
33
36
|
if (simplify_by_default !== undefined)
|
|
34
37
|
body.simplify_by_default = simplify_by_default;
|
|
38
|
+
const gate = previewUnlessConfirmed(confirm, `Create Splitwise group "${name}"`, 'POST', '/create_group', body);
|
|
39
|
+
if (gate)
|
|
40
|
+
return gate;
|
|
35
41
|
const data = await client.request('POST', '/create_group', body);
|
|
36
42
|
return textResult(data);
|
|
37
43
|
});
|
|
@@ -43,8 +49,9 @@ export function registerGroupTools(server) {
|
|
|
43
49
|
first_name: z.string().optional(),
|
|
44
50
|
last_name: z.string().optional(),
|
|
45
51
|
email: z.string().optional(),
|
|
52
|
+
confirm: schemaConfirm,
|
|
46
53
|
},
|
|
47
|
-
}, async ({ group_id, user_id, first_name, last_name, email }) => {
|
|
54
|
+
}, async ({ group_id, user_id, first_name, last_name, email, confirm }) => {
|
|
48
55
|
let body;
|
|
49
56
|
if (user_id !== undefined) {
|
|
50
57
|
body = { group_id, user_id };
|
|
@@ -55,26 +62,38 @@ export function registerGroupTools(server) {
|
|
|
55
62
|
}
|
|
56
63
|
body = { group_id, first_name, last_name, email };
|
|
57
64
|
}
|
|
65
|
+
const gate = previewUnlessConfirmed(confirm, `Add a user to Splitwise group ${group_id} (may send an invite email)`, 'POST', '/add_user_to_group', body);
|
|
66
|
+
if (gate)
|
|
67
|
+
return gate;
|
|
58
68
|
const data = await client.request('POST', '/add_user_to_group', body);
|
|
59
69
|
return textResult(data);
|
|
60
70
|
});
|
|
61
71
|
server.registerTool('sw_remove_user_from_group', {
|
|
62
|
-
description: 'Remove a user from a Splitwise group.',
|
|
72
|
+
description: 'Remove a user from a Splitwise group. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it removes the user.',
|
|
73
|
+
annotations: { destructiveHint: true },
|
|
63
74
|
inputSchema: {
|
|
64
75
|
group_id: z.number().describe('Group ID'),
|
|
65
76
|
user_id: z.number().describe('User ID to remove'),
|
|
77
|
+
confirm: schemaConfirm,
|
|
66
78
|
},
|
|
67
|
-
}, async ({ group_id, user_id }) => {
|
|
79
|
+
}, async ({ group_id, user_id, confirm }) => {
|
|
80
|
+
const gate = previewUnlessConfirmed(confirm, `Remove user ${user_id} from Splitwise group ${group_id}`, 'POST', '/remove_user_from_group', { group_id, user_id });
|
|
81
|
+
if (gate)
|
|
82
|
+
return gate;
|
|
68
83
|
const data = await client.request('POST', '/remove_user_from_group', { group_id, user_id });
|
|
69
84
|
return textResult(data);
|
|
70
85
|
});
|
|
71
86
|
server.registerTool('sw_delete_group', {
|
|
72
|
-
description: 'Soft-delete a Splitwise group.',
|
|
87
|
+
description: 'Soft-delete a Splitwise group. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.',
|
|
73
88
|
annotations: { destructiveHint: true },
|
|
74
89
|
inputSchema: {
|
|
75
90
|
id: z.number().describe('Group ID to delete'),
|
|
91
|
+
confirm: schemaConfirm,
|
|
76
92
|
},
|
|
77
|
-
}, async ({ id }) => {
|
|
93
|
+
}, async ({ id, confirm }) => {
|
|
94
|
+
const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise group ${id}`, 'POST', `/delete_group/${id}`);
|
|
95
|
+
if (gate)
|
|
96
|
+
return gate;
|
|
78
97
|
const data = await client.request('POST', `/delete_group/${id}`);
|
|
79
98
|
return textResult(data);
|
|
80
99
|
});
|
package/dist/tools/user.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from '../client.js';
|
|
4
|
+
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
4
5
|
export function registerUserTools(server) {
|
|
5
6
|
server.registerTool('sw_get_current_user', {
|
|
6
7
|
description: "Get the authenticated Splitwise user's profile (id, first_name, last_name, email). Use the returned id when building custom expense splits.",
|
|
@@ -29,8 +30,9 @@ export function registerUserTools(server) {
|
|
|
29
30
|
password: z.string().optional(),
|
|
30
31
|
locale: z.string().optional(),
|
|
31
32
|
default_currency: z.string().optional(),
|
|
33
|
+
confirm: schemaConfirm,
|
|
32
34
|
},
|
|
33
|
-
}, async ({ id, first_name, last_name, email, password, locale, default_currency }) => {
|
|
35
|
+
}, async ({ id, first_name, last_name, email, password, locale, default_currency, confirm }) => {
|
|
34
36
|
const body = {};
|
|
35
37
|
if (first_name !== undefined)
|
|
36
38
|
body.first_name = first_name;
|
|
@@ -44,6 +46,11 @@ export function registerUserTools(server) {
|
|
|
44
46
|
body.locale = locale;
|
|
45
47
|
if (default_currency !== undefined)
|
|
46
48
|
body.default_currency = default_currency;
|
|
49
|
+
// Never echo the password in the dry-run preview.
|
|
50
|
+
const previewBody = { ...body, ...(password !== undefined ? { password: '[hidden]' } : {}) };
|
|
51
|
+
const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, 'POST', `/update_user/${id}`, previewBody);
|
|
52
|
+
if (gate)
|
|
53
|
+
return gate;
|
|
47
54
|
const data = await client.request('POST', `/update_user/${id}`, body);
|
|
48
55
|
return textResult(data);
|
|
49
56
|
});
|
package/dist/tools/utilities.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { textResult, buildQueryString } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from '../client.js';
|
|
4
|
+
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
4
5
|
export function registerUtilityTools(server) {
|
|
5
6
|
server.registerTool('sw_get_notifications', {
|
|
6
7
|
description: 'Get recent Splitwise activity notifications for the current user.',
|
|
@@ -36,22 +37,31 @@ export function registerUtilityTools(server) {
|
|
|
36
37
|
return textResult(data);
|
|
37
38
|
});
|
|
38
39
|
server.registerTool('sw_create_comment', {
|
|
39
|
-
description: 'Add a comment to a Splitwise expense.',
|
|
40
|
+
description: 'Add a comment to a Splitwise expense (visible to other participants). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it posts.',
|
|
41
|
+
annotations: { destructiveHint: true },
|
|
40
42
|
inputSchema: {
|
|
41
43
|
expense_id: z.number().describe('Expense ID to comment on'),
|
|
42
44
|
content: z.string().describe('Comment text'),
|
|
45
|
+
confirm: schemaConfirm,
|
|
43
46
|
},
|
|
44
|
-
}, async ({ expense_id, content }) => {
|
|
47
|
+
}, async ({ expense_id, content, confirm }) => {
|
|
48
|
+
const gate = previewUnlessConfirmed(confirm, `Comment on Splitwise expense ${expense_id} (visible to participants)`, 'POST', '/create_comment', { expense_id, content });
|
|
49
|
+
if (gate)
|
|
50
|
+
return gate;
|
|
45
51
|
const data = await client.request('POST', '/create_comment', { expense_id, content });
|
|
46
52
|
return textResult(data);
|
|
47
53
|
});
|
|
48
54
|
server.registerTool('sw_delete_comment', {
|
|
49
|
-
description: 'Delete a comment by id.',
|
|
55
|
+
description: 'Delete a comment by id. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.',
|
|
50
56
|
annotations: { destructiveHint: true },
|
|
51
57
|
inputSchema: {
|
|
52
58
|
id: z.number().describe('Comment ID to delete'),
|
|
59
|
+
confirm: schemaConfirm,
|
|
53
60
|
},
|
|
54
|
-
}, async ({ id }) => {
|
|
61
|
+
}, async ({ id, confirm }) => {
|
|
62
|
+
const gate = previewUnlessConfirmed(confirm, `Delete Splitwise comment ${id}`, 'POST', `/delete_comment/${id}`);
|
|
63
|
+
if (gate)
|
|
64
|
+
return gate;
|
|
55
65
|
const data = await client.request('POST', `/delete_comment/${id}`);
|
|
56
66
|
return textResult(data);
|
|
57
67
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splitwise-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.12",
|
|
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>",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
32
|
".claude-plugin",
|
|
33
|
-
"
|
|
33
|
+
"skills/",
|
|
34
34
|
".mcp.json",
|
|
35
35
|
"server.json"
|
|
36
36
|
],
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"test:coverage": "vitest run --coverage"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@chrischall/mcp-utils": "^0.
|
|
46
|
+
"@chrischall/mcp-utils": "^0.13.0",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
48
|
"dotenv": "^17.4.0",
|
|
49
49
|
"zod": "^4.4.2"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@types/node": "^26.0.0",
|
|
53
53
|
"@vitest/coverage-v8": "^4.1.2",
|
|
54
54
|
"esbuild": "^0.28.0",
|
|
55
|
-
"typescript": "^
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
56
|
"vitest": "^4.1.2"
|
|
57
57
|
}
|
|
58
58
|
}
|
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.0.12",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "splitwise-mcp",
|
|
14
|
-
"version": "2.0.
|
|
14
|
+
"version": "2.0.12",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
File without changes
|