splitwise-mcp 2.0.9 → 2.0.11
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 +136 -37
- 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 +3 -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.0.11"
|
|
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.11",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/bundle.js
CHANGED
|
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
-
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw mod = 0, e;
|
|
13
|
+
}
|
|
10
14
|
};
|
|
11
15
|
var __export = (target, all) => {
|
|
12
16
|
for (var name in all)
|
|
@@ -31017,8 +31021,12 @@ var API_KEY_RE = new RegExp([
|
|
|
31017
31021
|
// webhook signing secret (Stripe-style)
|
|
31018
31022
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
31019
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");
|
|
31020
31028
|
function redactSecrets(text) {
|
|
31021
|
-
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]");
|
|
31022
31030
|
}
|
|
31023
31031
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
31024
31032
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
@@ -31065,6 +31073,18 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
31065
31073
|
}
|
|
31066
31074
|
}
|
|
31067
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
|
+
|
|
31068
31088
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
31069
31089
|
var DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
31070
31090
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -31156,7 +31176,11 @@ function createApiClient(opts) {
|
|
|
31156
31176
|
const res = await once();
|
|
31157
31177
|
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
31158
31178
|
attempt += 1;
|
|
31159
|
-
|
|
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);
|
|
31160
31184
|
continue;
|
|
31161
31185
|
}
|
|
31162
31186
|
return res;
|
|
@@ -31191,7 +31215,26 @@ function createApiClient(opts) {
|
|
|
31191
31215
|
}
|
|
31192
31216
|
return text;
|
|
31193
31217
|
}
|
|
31194
|
-
|
|
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 };
|
|
31195
31238
|
}
|
|
31196
31239
|
function buildQueryString(params) {
|
|
31197
31240
|
const parts = [];
|
|
@@ -31223,6 +31266,8 @@ var NonNegInt = external_exports.number().int().nonnegative();
|
|
|
31223
31266
|
var NonEmptyString = external_exports.string().min(1);
|
|
31224
31267
|
var IsoDate = external_exports.iso.date();
|
|
31225
31268
|
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
31269
|
+
var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
|
|
31270
|
+
var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
|
|
31226
31271
|
var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
|
|
31227
31272
|
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
31228
31273
|
var paginationSchema = {
|
|
@@ -31279,6 +31324,19 @@ var SplitwiseClient = class {
|
|
|
31279
31324
|
};
|
|
31280
31325
|
var client = new SplitwiseClient();
|
|
31281
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
|
+
|
|
31282
31340
|
// src/tools/user.ts
|
|
31283
31341
|
function registerUserTools(server) {
|
|
31284
31342
|
server.registerTool("sw_get_current_user", {
|
|
@@ -31307,9 +31365,10 @@ function registerUserTools(server) {
|
|
|
31307
31365
|
email: external_exports.string().optional(),
|
|
31308
31366
|
password: external_exports.string().optional(),
|
|
31309
31367
|
locale: external_exports.string().optional(),
|
|
31310
|
-
default_currency: external_exports.string().optional()
|
|
31368
|
+
default_currency: external_exports.string().optional(),
|
|
31369
|
+
confirm: schemaConfirm
|
|
31311
31370
|
}
|
|
31312
|
-
}, 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 }) => {
|
|
31313
31372
|
const body = {};
|
|
31314
31373
|
if (first_name !== void 0) body.first_name = first_name;
|
|
31315
31374
|
if (last_name !== void 0) body.last_name = last_name;
|
|
@@ -31317,6 +31376,9 @@ function registerUserTools(server) {
|
|
|
31317
31376
|
if (password !== void 0) body.password = password;
|
|
31318
31377
|
if (locale !== void 0) body.locale = locale;
|
|
31319
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;
|
|
31320
31382
|
const data = await client.request("POST", `/update_user/${id}`, body);
|
|
31321
31383
|
return textResult(data);
|
|
31322
31384
|
});
|
|
@@ -31343,15 +31405,19 @@ function registerGroupTools(server) {
|
|
|
31343
31405
|
});
|
|
31344
31406
|
server.registerTool("sw_create_group", {
|
|
31345
31407
|
description: "Create a new Splitwise group.",
|
|
31408
|
+
annotations: { destructiveHint: true },
|
|
31346
31409
|
inputSchema: {
|
|
31347
31410
|
name: external_exports.string().describe("Group name"),
|
|
31348
31411
|
group_type: external_exports.enum(["apartment", "house", "trip", "other"]).describe("Type of group").optional(),
|
|
31349
|
-
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
|
|
31350
31414
|
}
|
|
31351
|
-
}, async ({ name, group_type, simplify_by_default }) => {
|
|
31415
|
+
}, async ({ name, group_type, simplify_by_default, confirm }) => {
|
|
31352
31416
|
const body = { name };
|
|
31353
31417
|
if (group_type !== void 0) body.group_type = group_type;
|
|
31354
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;
|
|
31355
31421
|
const data = await client.request("POST", "/create_group", body);
|
|
31356
31422
|
return textResult(data);
|
|
31357
31423
|
});
|
|
@@ -31362,9 +31428,10 @@ function registerGroupTools(server) {
|
|
|
31362
31428
|
user_id: external_exports.number().describe("User ID (preferred)").optional(),
|
|
31363
31429
|
first_name: external_exports.string().optional(),
|
|
31364
31430
|
last_name: external_exports.string().optional(),
|
|
31365
|
-
email: external_exports.string().optional()
|
|
31431
|
+
email: external_exports.string().optional(),
|
|
31432
|
+
confirm: schemaConfirm
|
|
31366
31433
|
}
|
|
31367
|
-
}, async ({ group_id, user_id, first_name, last_name, email: email3 }) => {
|
|
31434
|
+
}, async ({ group_id, user_id, first_name, last_name, email: email3, confirm }) => {
|
|
31368
31435
|
let body;
|
|
31369
31436
|
if (user_id !== void 0) {
|
|
31370
31437
|
body = { group_id, user_id };
|
|
@@ -31374,26 +31441,35 @@ function registerGroupTools(server) {
|
|
|
31374
31441
|
}
|
|
31375
31442
|
body = { group_id, first_name, last_name, email: email3 };
|
|
31376
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;
|
|
31377
31446
|
const data = await client.request("POST", "/add_user_to_group", body);
|
|
31378
31447
|
return textResult(data);
|
|
31379
31448
|
});
|
|
31380
31449
|
server.registerTool("sw_remove_user_from_group", {
|
|
31381
|
-
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 },
|
|
31382
31452
|
inputSchema: {
|
|
31383
31453
|
group_id: external_exports.number().describe("Group ID"),
|
|
31384
|
-
user_id: external_exports.number().describe("User ID to remove")
|
|
31454
|
+
user_id: external_exports.number().describe("User ID to remove"),
|
|
31455
|
+
confirm: schemaConfirm
|
|
31385
31456
|
}
|
|
31386
|
-
}, 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;
|
|
31387
31460
|
const data = await client.request("POST", "/remove_user_from_group", { group_id, user_id });
|
|
31388
31461
|
return textResult(data);
|
|
31389
31462
|
});
|
|
31390
31463
|
server.registerTool("sw_delete_group", {
|
|
31391
|
-
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.",
|
|
31392
31465
|
annotations: { destructiveHint: true },
|
|
31393
31466
|
inputSchema: {
|
|
31394
|
-
id: external_exports.number().describe("Group ID to delete")
|
|
31467
|
+
id: external_exports.number().describe("Group ID to delete"),
|
|
31468
|
+
confirm: schemaConfirm
|
|
31395
31469
|
}
|
|
31396
|
-
}, 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;
|
|
31397
31473
|
const data = await client.request("POST", `/delete_group/${id}`);
|
|
31398
31474
|
return textResult(data);
|
|
31399
31475
|
});
|
|
@@ -31418,26 +31494,33 @@ function registerFriendTools(server) {
|
|
|
31418
31494
|
return textResult(data);
|
|
31419
31495
|
});
|
|
31420
31496
|
server.registerTool("sw_create_friend", {
|
|
31421
|
-
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 },
|
|
31422
31499
|
inputSchema: {
|
|
31423
31500
|
user_email: external_exports.string().describe("Email of the user to add as a friend"),
|
|
31424
31501
|
user_first_name: external_exports.string().describe("First name of the user").optional(),
|
|
31425
|
-
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
|
|
31426
31504
|
}
|
|
31427
|
-
}, async ({ user_email, user_first_name, user_last_name }) => {
|
|
31505
|
+
}, async ({ user_email, user_first_name, user_last_name, confirm }) => {
|
|
31428
31506
|
const body = { user_email };
|
|
31429
31507
|
if (user_first_name !== void 0) body.user_first_name = user_first_name;
|
|
31430
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;
|
|
31431
31511
|
const data = await client.request("POST", "/create_friend", body);
|
|
31432
31512
|
return textResult(data);
|
|
31433
31513
|
});
|
|
31434
31514
|
server.registerTool("sw_delete_friend", {
|
|
31435
|
-
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.",
|
|
31436
31516
|
annotations: { destructiveHint: true },
|
|
31437
31517
|
inputSchema: {
|
|
31438
|
-
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
|
|
31439
31520
|
}
|
|
31440
|
-
}, 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;
|
|
31441
31524
|
const data = await client.request("POST", `/delete_friend/${id}`);
|
|
31442
31525
|
return textResult(data);
|
|
31443
31526
|
});
|
|
@@ -31454,7 +31537,7 @@ function flattenUsers(users) {
|
|
|
31454
31537
|
return flat;
|
|
31455
31538
|
}
|
|
31456
31539
|
function buildExpenseBody(args) {
|
|
31457
|
-
const { split_equally, users, expense_id: _id, ...rest } = args;
|
|
31540
|
+
const { split_equally, users, expense_id: _id, confirm: _confirm, ...rest } = args;
|
|
31458
31541
|
if (split_equally && users) {
|
|
31459
31542
|
throw new Error("Provide either split_equally or users, not both");
|
|
31460
31543
|
}
|
|
@@ -31510,7 +31593,7 @@ function registerExpenseTools(server) {
|
|
|
31510
31593
|
return textResult(data);
|
|
31511
31594
|
});
|
|
31512
31595
|
server.registerTool("sw_create_expense", {
|
|
31513
|
-
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.',
|
|
31514
31597
|
inputSchema: {
|
|
31515
31598
|
group_id: external_exports.number().describe("Group to add expense to (use 0 for no group)"),
|
|
31516
31599
|
description: external_exports.string().describe("Short description of the expense"),
|
|
@@ -31520,15 +31603,18 @@ function registerExpenseTools(server) {
|
|
|
31520
31603
|
currency_code: external_exports.string().describe('Currency code, e.g. "USD". Defaults to group/user default.').optional(),
|
|
31521
31604
|
date: external_exports.string().describe("ISO 8601 datetime").optional(),
|
|
31522
31605
|
category_id: external_exports.number().describe("Category id from sw_get_categories").optional(),
|
|
31523
|
-
details: external_exports.string().describe("Notes").optional()
|
|
31606
|
+
details: external_exports.string().describe("Notes").optional(),
|
|
31607
|
+
confirm: schemaConfirm
|
|
31524
31608
|
}
|
|
31525
31609
|
}, async (args) => {
|
|
31526
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;
|
|
31527
31613
|
const data = await client.request("POST", "/create_expense", body);
|
|
31528
31614
|
return textResult(data);
|
|
31529
31615
|
});
|
|
31530
31616
|
server.registerTool("sw_update_expense", {
|
|
31531
|
-
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.",
|
|
31532
31618
|
inputSchema: {
|
|
31533
31619
|
expense_id: external_exports.number().describe("ID of the expense to update"),
|
|
31534
31620
|
description: external_exports.string().optional(),
|
|
@@ -31538,21 +31624,27 @@ function registerExpenseTools(server) {
|
|
|
31538
31624
|
currency_code: external_exports.string().optional(),
|
|
31539
31625
|
date: external_exports.string().optional(),
|
|
31540
31626
|
category_id: external_exports.number().optional(),
|
|
31541
|
-
details: external_exports.string().optional()
|
|
31627
|
+
details: external_exports.string().optional(),
|
|
31628
|
+
confirm: schemaConfirm
|
|
31542
31629
|
}
|
|
31543
31630
|
}, async (args) => {
|
|
31544
31631
|
const { expense_id } = args;
|
|
31545
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;
|
|
31546
31635
|
const data = await client.request("POST", `/update_expense/${expense_id}`, body);
|
|
31547
31636
|
return textResult(data);
|
|
31548
31637
|
});
|
|
31549
31638
|
server.registerTool("sw_delete_expense", {
|
|
31550
|
-
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.",
|
|
31551
31640
|
annotations: { destructiveHint: true },
|
|
31552
31641
|
inputSchema: {
|
|
31553
|
-
id: external_exports.number().describe("Expense ID to delete")
|
|
31642
|
+
id: external_exports.number().describe("Expense ID to delete"),
|
|
31643
|
+
confirm: schemaConfirm
|
|
31554
31644
|
}
|
|
31555
|
-
}, 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;
|
|
31556
31648
|
const data = await client.request("POST", `/delete_expense/${id}`);
|
|
31557
31649
|
return textResult(data);
|
|
31558
31650
|
});
|
|
@@ -31601,22 +31693,29 @@ function registerUtilityTools(server) {
|
|
|
31601
31693
|
return textResult(data);
|
|
31602
31694
|
});
|
|
31603
31695
|
server.registerTool("sw_create_comment", {
|
|
31604
|
-
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 },
|
|
31605
31698
|
inputSchema: {
|
|
31606
31699
|
expense_id: external_exports.number().describe("Expense ID to comment on"),
|
|
31607
|
-
content: external_exports.string().describe("Comment text")
|
|
31700
|
+
content: external_exports.string().describe("Comment text"),
|
|
31701
|
+
confirm: schemaConfirm
|
|
31608
31702
|
}
|
|
31609
|
-
}, 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;
|
|
31610
31706
|
const data = await client.request("POST", "/create_comment", { expense_id, content });
|
|
31611
31707
|
return textResult(data);
|
|
31612
31708
|
});
|
|
31613
31709
|
server.registerTool("sw_delete_comment", {
|
|
31614
|
-
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.",
|
|
31615
31711
|
annotations: { destructiveHint: true },
|
|
31616
31712
|
inputSchema: {
|
|
31617
|
-
id: external_exports.number().describe("Comment ID to delete")
|
|
31713
|
+
id: external_exports.number().describe("Comment ID to delete"),
|
|
31714
|
+
confirm: schemaConfirm
|
|
31618
31715
|
}
|
|
31619
|
-
}, async ({ id }) => {
|
|
31716
|
+
}, async ({ id, confirm }) => {
|
|
31717
|
+
const gate = previewUnlessConfirmed(confirm, `Delete Splitwise comment ${id}`, "POST", `/delete_comment/${id}`);
|
|
31718
|
+
if (gate) return gate;
|
|
31620
31719
|
const data = await client.request("POST", `/delete_comment/${id}`);
|
|
31621
31720
|
return textResult(data);
|
|
31622
31721
|
});
|
|
@@ -31625,7 +31724,7 @@ function registerUtilityTools(server) {
|
|
|
31625
31724
|
// src/index.ts
|
|
31626
31725
|
await runMcp({
|
|
31627
31726
|
name: "splitwise-mcp",
|
|
31628
|
-
version: "2.0.
|
|
31727
|
+
version: "2.0.11",
|
|
31629
31728
|
// x-release-please-version
|
|
31630
31729
|
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
|
31631
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.11', // 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.11",
|
|
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>",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"test:coverage": "vitest run --coverage"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@chrischall/mcp-utils": "^0.
|
|
46
|
+
"@chrischall/mcp-utils": "^0.12.0",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
48
|
"dotenv": "^17.4.0",
|
|
49
49
|
"zod": "^4.4.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@types/node": "^
|
|
52
|
+
"@types/node": "^26.0.0",
|
|
53
53
|
"@vitest/coverage-v8": "^4.1.2",
|
|
54
54
|
"esbuild": "^0.28.0",
|
|
55
55
|
"typescript": "^6.0.2",
|
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.11",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "splitwise-mcp",
|
|
14
|
-
"version": "2.0.
|
|
14
|
+
"version": "2.0.11",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|