splitwise-mcp 3.1.0 → 3.1.2

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.
@@ -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": "3.1.0"
10
+ "version": "3.1.2"
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": "3.1.0",
18
+ "version": "3.1.2",
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": "3.1.0",
4
+ "version": "3.1.2",
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",
package/README.md CHANGED
@@ -126,7 +126,7 @@ Add to Claude Desktop config:
126
126
  | `sw_get_current_user` | Get the authenticated user's profile |
127
127
  | `sw_healthcheck` | Verify the API key and Splitwise reachability; says which of the two failed |
128
128
  | `sw_get_user` | Get another user's profile by ID |
129
- | `sw_update_user` | Update the current user's profile fields |
129
+ | `sw_update_user` | Update the current user's name, locale or default currency (login email and password are not changeable here) |
130
130
 
131
131
  ### Groups
132
132
 
package/dist/bundle.js CHANGED
@@ -4859,7 +4859,7 @@ var require_memoizer = __commonJS({
4859
4859
  var NONE2 = 0;
4860
4860
  var ASSUMED2 = 1;
4861
4861
  var PROVEN2 = 2;
4862
- function isRecursive2(inst, stack, resolve2) {
4862
+ function isRecursive2(inst, stack, resolve3) {
4863
4863
  const cached2 = recursive2.get(inst);
4864
4864
  if (cached2 !== void 0)
4865
4865
  return cached2 ? PROVEN2 : NONE2;
@@ -4869,7 +4869,7 @@ var require_memoizer = __commonJS({
4869
4869
  let result = NONE2;
4870
4870
  const check2 = (child) => {
4871
4871
  if (result !== PROVEN2 && child?._zod) {
4872
- const answer = isRecursive2(child, stack, resolve2);
4872
+ const answer = isRecursive2(child, stack, resolve3);
4873
4873
  if (answer > result)
4874
4874
  result = answer;
4875
4875
  }
@@ -4880,7 +4880,7 @@ var require_memoizer = __commonJS({
4880
4880
  const desc = Object.getOwnPropertyDescriptor(sh, key);
4881
4881
  if (spread && !desc.enumerable)
4882
4882
  continue;
4883
- const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve2) : NONE2;
4883
+ const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve3) : NONE2;
4884
4884
  if (child > answer)
4885
4885
  answer = child;
4886
4886
  }
@@ -4944,7 +4944,7 @@ var require_memoizer = __commonJS({
4944
4944
  break;
4945
4945
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
4946
4946
  case "lazy": {
4947
- const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
4947
+ const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
4948
4948
  merge2(inner ? isRecursive2(inner, stack, false) : ASSUMED2);
4949
4949
  break;
4950
4950
  }
@@ -62953,7 +62953,7 @@ async function extractImages$1(data, pageNumber) {
62953
62953
  for (let i2 = 0; i2 < operatorList.fnArray.length; i2++) {
62954
62954
  if (operatorList.fnArray[i2] !== OPS.paintImageXObject) continue;
62955
62955
  const imageKey = operatorList.argsArray[i2][0];
62956
- const image = await new Promise((resolve2) => (imageKey.startsWith("g_") ? page.commonObjs : page.objs).get(imageKey, resolve2));
62956
+ const image = await new Promise((resolve3) => (imageKey.startsWith("g_") ? page.commonObjs : page.objs).get(imageKey, resolve3));
62957
62957
  if (!image || !image.data || !image.width || !image.height) continue;
62958
62958
  const { width, height, data: data2 } = image;
62959
62959
  const calculatedChannels = data2.length / (width * height);
@@ -62998,8 +62998,8 @@ async function renderPageAsImage$1(data, pageNumber, options = {}) {
62998
62998
  const buffer = await canvas.encode("png");
62999
62999
  return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
63000
63000
  }
63001
- const blob = await new Promise((resolve2) => {
63002
- canvas.toBlob(resolve2);
63001
+ const blob = await new Promise((resolve3) => {
63002
+ canvas.toBlob(resolve3);
63003
63003
  });
63004
63004
  if (!blob) throw new Error("Failed to encode canvas to a PNG blob.");
63005
63005
  return await blob.arrayBuffer();
@@ -63199,6 +63199,31 @@ var init_dist = __esm({
63199
63199
  }
63200
63200
  });
63201
63201
 
63202
+ // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
63203
+ import { AsyncLocalStorage } from "node:async_hooks";
63204
+ var storage = new AsyncLocalStorage();
63205
+ function withCallSignal(signal, fn2, request) {
63206
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn2) : fn2();
63207
+ }
63208
+ function currentCallSignal() {
63209
+ return storage.getStore()?.signal;
63210
+ }
63211
+ function withAmbientCancellation(own2) {
63212
+ const ambient = currentCallSignal();
63213
+ if (!ambient)
63214
+ return own2;
63215
+ if (!own2)
63216
+ return ambient;
63217
+ return AbortSignal.any([own2, ambient]);
63218
+ }
63219
+
63220
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
63221
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
63222
+ var storage2 = new AsyncLocalStorage2();
63223
+ function withCallerCapabilities(capabilities, fn2) {
63224
+ return capabilities ? storage2.run(capabilities, fn2) : fn2();
63225
+ }
63226
+
63202
63227
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
63203
63228
  var __create2 = Object.create;
63204
63229
  var __defProp2 = Object.defineProperty;
@@ -67601,14 +67626,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
67601
67626
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
67602
67627
  }
67603
67628
  function sleep(ms2, signal) {
67604
- return new Promise((resolve2, reject) => {
67629
+ return new Promise((resolve3, reject) => {
67605
67630
  if (signal?.aborted) {
67606
67631
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
67607
67632
  return;
67608
67633
  }
67609
67634
  const timer = setTimeout(() => {
67610
67635
  signal?.removeEventListener("abort", onAbort);
67611
- resolve2();
67636
+ resolve3();
67612
67637
  }, ms2);
67613
67638
  const onAbort = () => {
67614
67639
  clearTimeout(timer);
@@ -68402,7 +68427,7 @@ var Protocol = class {
68402
68427
  const flowStartedAt = Date.now();
68403
68428
  let onAbort;
68404
68429
  let cleanupMessageId;
68405
- return new Promise((resolve2, reject) => {
68430
+ return new Promise((resolve3, reject) => {
68406
68431
  const earlyReject = (error62) => {
68407
68432
  reject(error62);
68408
68433
  };
@@ -68470,7 +68495,7 @@ var Protocol = class {
68470
68495
  }
68471
68496
  if (decoded.kind === "invalid") return reject(decoded.error);
68472
68497
  if (decoded.kind === "input_required") {
68473
- if (options?.allowInputRequired === true) return resolve2(manualInputRequiredValue(decoded));
68498
+ if (options?.allowInputRequired === true) return resolve3(manualInputRequiredValue(decoded));
68474
68499
  const flow = {
68475
68500
  codec: codec2,
68476
68501
  request,
@@ -68482,11 +68507,11 @@ var Protocol = class {
68482
68507
  params
68483
68508
  }, resultSchema, legOptions)
68484
68509
  };
68485
- return resolve2(this._resolveNonCompleteResult(decoded, flow));
68510
+ return resolve3(this._resolveNonCompleteResult(decoded, flow));
68486
68511
  }
68487
68512
  const result = decoded.result;
68488
68513
  validateStandardSchema(resultSchema, result).then((parseResult) => {
68489
- if (parseResult.success) resolve2(parseResult.data);
68514
+ if (parseResult.success) resolve3(parseResult.data);
68490
68515
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
68491
68516
  }, reject);
68492
68517
  });
@@ -71398,7 +71423,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
71398
71423
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
71399
71424
  const schOrFunc = root.refs[ref];
71400
71425
  if (schOrFunc) return schOrFunc;
71401
- let _sch = resolve2.call(this, root, ref);
71426
+ let _sch = resolve3.call(this, root, ref);
71402
71427
  if (_sch === void 0) {
71403
71428
  const schema = (_a4 = root.localRefs) === null || _a4 === void 0 ? void 0 : _a4[ref];
71404
71429
  const { schemaId } = this.opts;
@@ -71424,7 +71449,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
71424
71449
  function sameSchemaEnv(s1, s2) {
71425
71450
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
71426
71451
  }
71427
- function resolve2(root, ref) {
71452
+ function resolve3(root, ref) {
71428
71453
  let sch;
71429
71454
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
71430
71455
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -71874,47 +71899,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
71874
71899
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
71875
71900
  return uri;
71876
71901
  }
71877
- function resolve2(baseURI, relativeURI, options) {
71902
+ function resolve3(baseURI, relativeURI, options) {
71878
71903
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
71879
71904
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
71880
71905
  schemelessOptions.skipEscape = true;
71881
71906
  return serialize(resolved, schemelessOptions);
71882
71907
  }
71883
- function resolveComponent(base, relative, options, skipNormalization) {
71908
+ function resolveComponent(base, relative2, options, skipNormalization) {
71884
71909
  const target = {};
71885
71910
  if (!skipNormalization) {
71886
71911
  base = parse3(serialize(base, options), options);
71887
- relative = parse3(serialize(relative, options), options);
71912
+ relative2 = parse3(serialize(relative2, options), options);
71888
71913
  }
71889
71914
  options = options || {};
71890
- if (!options.tolerant && relative.scheme) {
71891
- target.scheme = relative.scheme;
71892
- target.userinfo = relative.userinfo;
71893
- target.host = relative.host;
71894
- target.port = relative.port;
71895
- target.path = removeDotSegments(relative.path || "");
71896
- target.query = relative.query;
71915
+ if (!options.tolerant && relative2.scheme) {
71916
+ target.scheme = relative2.scheme;
71917
+ target.userinfo = relative2.userinfo;
71918
+ target.host = relative2.host;
71919
+ target.port = relative2.port;
71920
+ target.path = removeDotSegments(relative2.path || "");
71921
+ target.query = relative2.query;
71897
71922
  } else {
71898
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
71899
- target.userinfo = relative.userinfo;
71900
- target.host = relative.host;
71901
- target.port = relative.port;
71902
- target.path = removeDotSegments(relative.path || "");
71903
- target.query = relative.query;
71923
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
71924
+ target.userinfo = relative2.userinfo;
71925
+ target.host = relative2.host;
71926
+ target.port = relative2.port;
71927
+ target.path = removeDotSegments(relative2.path || "");
71928
+ target.query = relative2.query;
71904
71929
  } else {
71905
- if (!relative.path) {
71930
+ if (!relative2.path) {
71906
71931
  target.path = base.path;
71907
- if (relative.query !== void 0) target.query = relative.query;
71932
+ if (relative2.query !== void 0) target.query = relative2.query;
71908
71933
  else target.query = base.query;
71909
71934
  } else {
71910
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
71935
+ if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
71911
71936
  else {
71912
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
71913
- else if (!base.path) target.path = relative.path;
71914
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
71937
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
71938
+ else if (!base.path) target.path = relative2.path;
71939
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
71915
71940
  target.path = removeDotSegments(target.path);
71916
71941
  }
71917
- target.query = relative.query;
71942
+ target.query = relative2.query;
71918
71943
  }
71919
71944
  target.userinfo = base.userinfo;
71920
71945
  target.host = base.host;
@@ -71922,7 +71947,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
71922
71947
  }
71923
71948
  target.scheme = base.scheme;
71924
71949
  }
71925
- target.fragment = relative.fragment;
71950
+ target.fragment = relative2.fragment;
71926
71951
  return target;
71927
71952
  }
71928
71953
  function equal(uriA, uriB, options) {
@@ -72048,7 +72073,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
72048
72073
  const fastUri = {
72049
72074
  SCHEMES,
72050
72075
  normalize,
72051
- resolve: resolve2,
72076
+ resolve: resolve3,
72052
72077
  resolveComponent,
72053
72078
  equal,
72054
72079
  serialize,
@@ -77421,7 +77446,7 @@ var StdioServerTransport = class {
77421
77446
  }
77422
77447
  send(message) {
77423
77448
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
77424
- return new Promise((resolve2, reject) => {
77449
+ return new Promise((resolve3, reject) => {
77425
77450
  const json2 = serializeMessage(message);
77426
77451
  let settled = false;
77427
77452
  const onError = (error62) => {
@@ -77436,14 +77461,14 @@ var StdioServerTransport = class {
77436
77461
  settled = true;
77437
77462
  this._stdout.off("error", onError);
77438
77463
  this._stdout.off("drain", onDrain);
77439
- resolve2();
77464
+ resolve3();
77440
77465
  };
77441
77466
  this._stdout.once("error", onError);
77442
77467
  if (this._stdout.write(json2)) {
77443
77468
  if (settled) return;
77444
77469
  settled = true;
77445
77470
  this._stdout.off("error", onError);
77446
- resolve2();
77471
+ resolve3();
77447
77472
  } else if (!settled) this._stdout.once("drain", onDrain);
77448
77473
  });
77449
77474
  }
@@ -77497,14 +77522,14 @@ var StdioConnectionChannel = class {
77497
77522
  */
77498
77523
  async whenRequestsAnswered(timeoutMs) {
77499
77524
  if (this._closed || this._pendingRequests.size === 0) return true;
77500
- return await new Promise((resolve2) => {
77525
+ return await new Promise((resolve3) => {
77501
77526
  const waiter = () => {
77502
77527
  clearTimeout(timer);
77503
- resolve2(true);
77528
+ resolve3(true);
77504
77529
  };
77505
77530
  const timer = setTimeout(() => {
77506
77531
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
77507
- resolve2(false);
77532
+ resolve3(false);
77508
77533
  }, timeoutMs);
77509
77534
  this._drainWaiters.push(waiter);
77510
77535
  });
@@ -77875,13 +77900,28 @@ var API_KEY_RE = new RegExp([
77875
77900
  "whsec_[A-Za-z0-9]{16,}\\b"
77876
77901
  // webhook signing secret (Stripe-style)
77877
77902
  ].map((p2) => `\\b${p2}`).join("|"), "g");
77878
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
77903
+ var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
77904
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
77905
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
77906
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
77879
77907
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
77880
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
77881
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
77882
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
77908
+ var JSON_SECRET_KEYS = [
77909
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
77910
+ "client[_-]?secret",
77911
+ "api[_-]?secret",
77912
+ "(?:x[_-])?api[_-]?key",
77913
+ "private[_-]?key",
77914
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
77915
+ "(?:proxy-)?authorization",
77916
+ "password",
77917
+ "passwd",
77918
+ "secret",
77919
+ "token"
77920
+ ].join("|");
77921
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
77922
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
77883
77923
  function redactSecrets(text) {
77884
- 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]");
77924
+ 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(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_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]");
77885
77925
  }
77886
77926
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
77887
77927
  const str = text === null || text === void 0 ? "" : String(text);
@@ -82922,7 +82962,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
82922
82962
  var NONE = 0;
82923
82963
  var ASSUMED = 1;
82924
82964
  var PROVEN = 2;
82925
- function isRecursive(inst, stack, resolve2) {
82965
+ function isRecursive(inst, stack, resolve3) {
82926
82966
  const cached2 = recursive.get(inst);
82927
82967
  if (cached2 !== void 0)
82928
82968
  return cached2 ? PROVEN : NONE;
@@ -82932,7 +82972,7 @@ function isRecursive(inst, stack, resolve2) {
82932
82972
  let result = NONE;
82933
82973
  const check2 = (child) => {
82934
82974
  if (result !== PROVEN && child?._zod) {
82935
- const answer = isRecursive(child, stack, resolve2);
82975
+ const answer = isRecursive(child, stack, resolve3);
82936
82976
  if (answer > result)
82937
82977
  result = answer;
82938
82978
  }
@@ -82943,7 +82983,7 @@ function isRecursive(inst, stack, resolve2) {
82943
82983
  const desc = Object.getOwnPropertyDescriptor(sh, key);
82944
82984
  if (spread && !desc.enumerable)
82945
82985
  continue;
82946
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve2) : NONE;
82986
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
82947
82987
  if (child > answer)
82948
82988
  answer = child;
82949
82989
  }
@@ -83007,7 +83047,7 @@ function isRecursive(inst, stack, resolve2) {
83007
83047
  break;
83008
83048
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
83009
83049
  case "lazy": {
83010
- const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
83050
+ const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
83011
83051
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
83012
83052
  break;
83013
83053
  }
@@ -97689,17 +97729,73 @@ Hint: ${err.hint}`);
97689
97729
  }
97690
97730
  throw err;
97691
97731
  }
97732
+ function mcpRequestFrom(args) {
97733
+ const ctx = args.at(-1);
97734
+ if (typeof ctx !== "object" || ctx === null)
97735
+ return void 0;
97736
+ const req = ctx.mcpReq;
97737
+ if (typeof req !== "object" || req === null)
97738
+ return void 0;
97739
+ return typeof req.notify === "function" ? req : void 0;
97740
+ }
97741
+ function callSignalFrom(args) {
97742
+ const ctx = args.at(-1);
97743
+ if (typeof ctx !== "object" || ctx === null)
97744
+ return void 0;
97745
+ const req = ctx.mcpReq;
97746
+ if (typeof req !== "object" || req === null)
97747
+ return void 0;
97748
+ const signal = req.signal;
97749
+ return signal instanceof AbortSignal ? signal : void 0;
97750
+ }
97751
+ function declaredCapabilitiesFrom(server) {
97752
+ const inner = server.server;
97753
+ if (typeof inner !== "object" || inner === null)
97754
+ return void 0;
97755
+ const read = inner.getClientCapabilities;
97756
+ if (typeof read !== "function")
97757
+ return void 0;
97758
+ try {
97759
+ const declared = read.call(inner);
97760
+ return typeof declared === "object" && declared !== null ? declared : void 0;
97761
+ } catch {
97762
+ return void 0;
97763
+ }
97764
+ }
97692
97765
  function surfaceToolHints(server) {
97693
97766
  const register2 = server.registerTool.bind(server);
97694
- server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => {
97695
- let result;
97696
- try {
97697
- result = cb(...args);
97698
- } catch (err) {
97699
- return hintResultOrRethrow(err);
97700
- }
97701
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
97702
- });
97767
+ server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
97768
+ // THE CALLER'S CANCELLATION, made ambient for the whole handler
97769
+ // (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
97770
+ // measured against @modelcontextprotocol/server 2.0.0, a cancelled
97771
+ // call aborts `ctx.mcpReq.signal` with the caller's reason and the
97772
+ // handler runs to completion regardless — so the HTTP request stays
97773
+ // in flight, the child keeps burning metered CPU, and the upstream
97774
+ // keeps being hit for somebody who has gone. claude.ai sent 101
97775
+ // cancellations in the week to 2026-09-20.
97776
+ //
97777
+ // Here because this is the one wrapper every tool already passes
97778
+ // through: threading the signal by hand would mean editing several
97779
+ // hundred handlers and missing exactly the ones nobody edits.
97780
+ withCallSignal(callSignalFrom(args), () => (
97781
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
97782
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
97783
+ // the wrapper — a 2026-07-28 request answers for itself off its own
97784
+ // envelope — but a guarded tool that cannot tell whether the caller
97785
+ // can be shown a prompt returns one the SDK then refuses to deliver,
97786
+ // and that refusal reaches the caller as an unexplained protocol
97787
+ // error it had no chance to catch.
97788
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
97789
+ let result;
97790
+ try {
97791
+ result = cb(...args);
97792
+ } catch (err) {
97793
+ return hintResultOrRethrow(err);
97794
+ }
97795
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
97796
+ })
97797
+ ), mcpRequestFrom(args))
97798
+ ));
97703
97799
  }
97704
97800
  async function createMcpServer(opts) {
97705
97801
  const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
@@ -97714,13 +97810,47 @@ async function createMcpServer(opts) {
97714
97810
  }
97715
97811
  return server;
97716
97812
  }
97813
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
97814
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
97717
97815
  function withGracefulShutdown(target, opts = {}) {
97718
97816
  const shouldExit = opts.exit ?? true;
97817
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
97818
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
97819
+ }
97820
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
97821
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
97822
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
97823
+ }
97824
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
97719
97825
  let shuttingDown = false;
97826
+ let firstSignalAt = 0;
97827
+ let exited = false;
97828
+ const exitOnce = () => {
97829
+ if (exited)
97830
+ return;
97831
+ exited = true;
97832
+ process.exit(0);
97833
+ };
97720
97834
  const handler = (signal) => {
97721
- if (shuttingDown)
97835
+ if (shuttingDown) {
97836
+ if (Date.now() - firstSignalAt < repeatGraceMs)
97837
+ return;
97838
+ if (shouldExit) {
97839
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
97840
+ exitOnce();
97841
+ }
97722
97842
  return;
97843
+ }
97723
97844
  shuttingDown = true;
97845
+ firstSignalAt = Date.now();
97846
+ let timer;
97847
+ if (shouldExit) {
97848
+ timer = setTimeout(() => {
97849
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
97850
+ exitOnce();
97851
+ }, timeoutMs);
97852
+ timer.unref?.();
97853
+ }
97724
97854
  void (async () => {
97725
97855
  try {
97726
97856
  if (opts.onSignal)
@@ -97729,8 +97859,10 @@ function withGracefulShutdown(target, opts = {}) {
97729
97859
  } catch (err) {
97730
97860
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
97731
97861
  } finally {
97862
+ if (timer !== void 0)
97863
+ clearTimeout(timer);
97732
97864
  if (shouldExit)
97733
- process.exit(0);
97865
+ exitOnce();
97734
97866
  }
97735
97867
  })();
97736
97868
  };
@@ -97800,14 +97932,55 @@ async function loadDotenvSafely(opts = {}) {
97800
97932
  }
97801
97933
  }
97802
97934
 
97935
+ // node_modules/@chrischall/mcp-utils/dist/fs/confine.js
97936
+ import { existsSync, realpathSync } from "node:fs";
97937
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
97938
+ function isInside(root, target) {
97939
+ const rel = relative(root, target);
97940
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
97941
+ }
97942
+ function realPathAllowingMissing(p2) {
97943
+ const abs = resolve2(p2);
97944
+ let existing = abs;
97945
+ const tail = [];
97946
+ while (!existsSync(existing)) {
97947
+ const parent = dirname(existing);
97948
+ if (parent === existing)
97949
+ break;
97950
+ tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
97951
+ existing = parent;
97952
+ }
97953
+ let real;
97954
+ try {
97955
+ real = realpathSync(existing);
97956
+ } catch {
97957
+ real = existing;
97958
+ }
97959
+ return tail.length > 0 ? resolve2(real, ...tail) : real;
97960
+ }
97961
+ function assertPathWithinRoots(path, roots) {
97962
+ const target = realPathAllowingMissing(expandPath(path));
97963
+ for (const root of roots) {
97964
+ if (isInside(realPathAllowingMissing(expandPath(root)), target))
97965
+ return target;
97966
+ }
97967
+ throw new Error(`Path is outside the allowed directories: ${path}`);
97968
+ }
97969
+
97803
97970
  // node_modules/@chrischall/mcp-utils/dist/fs/output.js
97804
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
97971
+ import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
97805
97972
  import { join as join2 } from "node:path";
97806
97973
  function resolveOutputDir(perCall, envVar, opts = {}) {
97807
97974
  const raw = perCall ?? readEnvVar(envVar, opts.env ? { env: opts.env } : {});
97808
97975
  if (!raw)
97809
97976
  return process.cwd();
97810
97977
  const dir = expandPath(raw);
97978
+ if (perCall !== void 0 && opts.allowedRoots) {
97979
+ assertPathWithinRoots(dir, opts.allowedRoots);
97980
+ mkdirSync(dir, { recursive: true });
97981
+ assertPathWithinRoots(dir, opts.allowedRoots);
97982
+ return dir;
97983
+ }
97811
97984
  mkdirSync(dir, { recursive: true });
97812
97985
  return dir;
97813
97986
  }
@@ -97818,11 +97991,11 @@ function sanitizeBaseName(base) {
97818
97991
  function uniquePath(dir, base, ext) {
97819
97992
  const safe = sanitizeBaseName(base);
97820
97993
  const first = join2(dir, `${safe}.${ext}`);
97821
- if (!existsSync(first))
97994
+ if (!existsSync2(first))
97822
97995
  return first;
97823
97996
  for (let n2 = 2; ; n2++) {
97824
97997
  const candidate = join2(dir, `${safe}-${n2}.${ext}`);
97825
- if (!existsSync(candidate))
97998
+ if (!existsSync2(candidate))
97826
97999
  return candidate;
97827
98000
  }
97828
98001
  }
@@ -97925,23 +98098,78 @@ function createApiClient(opts) {
97925
98098
  const retryStatuses = retry.statuses ?? [429];
97926
98099
  const timeoutMs = opts.timeout;
97927
98100
  const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
97928
- function withTimeout(run) {
97929
- if (timeoutMs == null || timeoutMs <= 0)
97930
- return run(void 0);
98101
+ async function withTimeout(run) {
98102
+ if (timeoutMs == null || timeoutMs <= 0) {
98103
+ const res2 = await run(withAmbientCancellation(void 0));
98104
+ return { res: res2, done: () => {
98105
+ }, expired: void 0 };
98106
+ }
97931
98107
  const controller = new AbortController();
97932
98108
  const timer = setTimeout(() => controller.abort(), timeoutMs);
97933
- return run(controller.signal).catch((err) => {
97934
- if (err instanceof Error && err.name === "AbortError")
98109
+ let res;
98110
+ try {
98111
+ res = await run(withAmbientCancellation(controller.signal));
98112
+ } catch (err) {
98113
+ clearTimeout(timer);
98114
+ if (err instanceof Error && err.name === "AbortError" && controller.signal.aborted) {
97935
98115
  throw new RequestTimeoutError(service, timeoutMs);
98116
+ }
97936
98117
  throw err;
97937
- }).finally(() => clearTimeout(timer));
98118
+ }
98119
+ const expired = new Promise((_resolve, reject) => {
98120
+ controller.signal.addEventListener("abort", () => reject(new RequestTimeoutError(service, timeoutMs)), {
98121
+ once: true
98122
+ });
98123
+ });
98124
+ expired.catch(() => {
98125
+ });
98126
+ return { res, done: () => clearTimeout(timer), expired };
98127
+ }
98128
+ async function readBody(attempt, read) {
98129
+ try {
98130
+ if (!attempt.expired)
98131
+ return await read(attempt.res);
98132
+ return await Promise.race([read(attempt.res), attempt.expired]);
98133
+ } catch (err) {
98134
+ if (err instanceof RequestTimeoutError) {
98135
+ attempt.res.body?.cancel().catch(() => {
98136
+ });
98137
+ }
98138
+ throw err;
98139
+ } finally {
98140
+ attempt.done();
98141
+ }
98142
+ }
98143
+ const baseParts = (() => {
98144
+ try {
98145
+ const u2 = new URL(base);
98146
+ return { origin: u2.origin, username: u2.username, password: u2.password };
98147
+ } catch {
98148
+ return void 0;
98149
+ }
98150
+ })();
98151
+ const baseOrigin = baseParts?.origin;
98152
+ function resolveUrl(path, query) {
98153
+ const url4 = `${base}${path}${query}`;
98154
+ if (baseOrigin === void 0)
98155
+ return url4;
98156
+ let parsed;
98157
+ try {
98158
+ parsed = new URL(url4);
98159
+ } catch {
98160
+ throw new Error(`Refusing request to ${service}: path ${JSON.stringify(path)} does not form a valid URL.`);
98161
+ }
98162
+ if (parsed.origin !== baseOrigin || parsed.username !== baseParts?.username || parsed.password !== baseParts?.password) {
98163
+ throw new Error(`Refusing request to ${service}: path ${JSON.stringify(path)} resolves outside the base origin ${baseOrigin}. Paths must be relative to baseUrl (start them with "/").`);
98164
+ }
98165
+ return url4;
97938
98166
  }
97939
98167
  async function send(method, path, opt) {
97940
98168
  const isMultipart = opt.formData !== void 0;
97941
98169
  const hasJsonBody = !isMultipart && opt.body !== void 0;
97942
98170
  const reqBody = isMultipart ? opt.formData : hasJsonBody ? JSON.stringify(opt.body) : void 0;
97943
98171
  const query = opt.query ? buildQueryString(opt.query) : "";
97944
- const url4 = `${base}${path}${query}`;
98172
+ const url4 = resolveUrl(path, query);
97945
98173
  const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
97946
98174
  const fetchWith = (token, signal) => doFetch(url4, {
97947
98175
  method,
@@ -97958,8 +98186,10 @@ function createApiClient(opts) {
97958
98186
  const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
97959
98187
  let attempt = 0;
97960
98188
  for (; ; ) {
97961
- const res = await once();
98189
+ const current = await once();
98190
+ const res = current.res;
97962
98191
  if (retryStatuses.includes(res.status) && attempt < retry.count) {
98192
+ current.done();
97963
98193
  attempt += 1;
97964
98194
  const delay = retry.honorRetryAfter ? parseRetryAfterMs(res.headers.get("retry-after"), {
97965
98195
  defaultMs: retry.delayMs,
@@ -97968,18 +98198,21 @@ function createApiClient(opts) {
97968
98198
  await sleep2(delay);
97969
98199
  continue;
97970
98200
  }
97971
- return res;
98201
+ return current;
97972
98202
  }
97973
98203
  }
97974
98204
  async function fetchJson(method, path, opt = {}) {
97975
- const res = await send(method, path, opt);
98205
+ const attempt = await send(method, path, opt);
98206
+ const res = attempt.res;
98207
+ if (res.status === 401 || res.status === 429 || res.status === 204)
98208
+ attempt.done();
97976
98209
  if (res.status === 401)
97977
98210
  throw unauthorized();
97978
98211
  if (res.status === 429)
97979
98212
  throw rateLimited();
97980
98213
  if (res.status === 204)
97981
98214
  return void 0;
97982
- const text = await res.text();
98215
+ const text = await readBody(attempt, (r2) => r2.text());
97983
98216
  if (!res.ok) {
97984
98217
  throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
97985
98218
  }
@@ -97989,12 +98222,15 @@ function createApiClient(opts) {
97989
98222
  }
97990
98223
  async function fetchHtml(method, path, opt = {}) {
97991
98224
  const headers = { Accept: "text/html,*/*", ...opt.headers };
97992
- const res = await send(method, path, { ...opt, headers });
98225
+ const attempt = await send(method, path, { ...opt, headers });
98226
+ const res = attempt.res;
98227
+ if (res.status === 401 || res.status === 429)
98228
+ attempt.done();
97993
98229
  if (res.status === 401)
97994
98230
  throw unauthorized();
97995
98231
  if (res.status === 429)
97996
98232
  throw rateLimited();
97997
- const text = await res.text();
98233
+ const text = await readBody(attempt, (r2) => r2.text());
97998
98234
  if (!res.ok) {
97999
98235
  throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
98000
98236
  }
@@ -98002,16 +98238,23 @@ function createApiClient(opts) {
98002
98238
  }
98003
98239
  async function fetchRaw(method, path, opt = {}) {
98004
98240
  const headers = { Accept: "*/*", ...opt.headers };
98005
- const res = await send(method, path, { ...opt, headers });
98241
+ const attempt = await send(method, path, { ...opt, headers });
98242
+ const res = attempt.res;
98243
+ if (res.status === 401 || res.status === 429)
98244
+ attempt.done();
98006
98245
  if (res.status === 401)
98007
98246
  throw unauthorized();
98008
98247
  if (res.status === 429)
98009
98248
  throw rateLimited();
98010
98249
  if (!res.ok) {
98011
- const text = await res.text().catch(() => "");
98250
+ const text = await readBody(attempt, (r2) => r2.text()).catch((err) => {
98251
+ if (err instanceof RequestTimeoutError)
98252
+ throw err;
98253
+ return "";
98254
+ });
98012
98255
  throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
98013
98256
  }
98014
- const bytes = new Uint8Array(await res.arrayBuffer());
98257
+ const bytes = new Uint8Array(await readBody(attempt, (r2) => r2.arrayBuffer()));
98015
98258
  return {
98016
98259
  status: res.status,
98017
98260
  contentType: res.headers.get("content-type"),
@@ -98065,10 +98308,10 @@ var pageSchema = {
98065
98308
  };
98066
98309
 
98067
98310
  // src/client.ts
98068
- import { dirname, join as join3 } from "path";
98311
+ import { dirname as dirname2, join as join3 } from "path";
98069
98312
  import { fileURLToPath } from "url";
98070
98313
  try {
98071
- const dir = dirname(fileURLToPath(import.meta.url));
98314
+ const dir = dirname2(fileURLToPath(import.meta.url));
98072
98315
  await loadDotenvSafely({ path: join3(dir, "..", ".env"), override: false });
98073
98316
  } catch {
98074
98317
  }
@@ -98097,6 +98340,25 @@ function redactAssetQuery(err, search) {
98097
98340
  if (message === err.message) return err;
98098
98341
  return err instanceof ApiError ? new ApiError(err.status, message) : new Error(message);
98099
98342
  }
98343
+ function flattenErrors(errors) {
98344
+ if (errors == null) return [];
98345
+ if (typeof errors === "string") return errors ? [errors] : [];
98346
+ if (Array.isArray(errors)) return errors.flatMap((e2) => flattenErrors(e2));
98347
+ if (typeof errors === "object") {
98348
+ return Object.entries(errors).flatMap(
98349
+ ([field, value]) => flattenErrors(value).map((msg) => field === "base" ? msg : `${field}: ${msg}`)
98350
+ );
98351
+ }
98352
+ return [String(errors)];
98353
+ }
98354
+ function assertWriteSucceeded(data) {
98355
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return;
98356
+ const record4 = data;
98357
+ const messages = flattenErrors(record4.errors);
98358
+ if (messages.length === 0 && record4.success !== false) return;
98359
+ const detail = messages.length > 0 ? messages.join("; ") : "success: false with no error detail";
98360
+ throw new Error(`Splitwise rejected the request: ${detail}`);
98361
+ }
98100
98362
  var SplitwiseClient = class {
98101
98363
  apiKey;
98102
98364
  /** Which source supplied the API key — a LABEL, never the value. */
@@ -98161,7 +98423,9 @@ var SplitwiseClient = class {
98161
98423
  return this.apiKey;
98162
98424
  }
98163
98425
  async request(method, path, body) {
98164
- return this.api.fetchJson(method, path, body !== void 0 ? { body } : {});
98426
+ const data = await this.api.fetchJson(method, path, body !== void 0 ? { body } : {});
98427
+ if (method.toUpperCase() !== "GET") assertWriteSucceeded(data);
98428
+ return data;
98165
98429
  }
98166
98430
  /**
98167
98431
  * Fetch the raw bytes behind an absolute asset URL the API handed back (an
@@ -98345,34 +98609,36 @@ function registerUserTools(server, client2) {
98345
98609
  server.registerTool(
98346
98610
  "sw_update_user",
98347
98611
  {
98348
- description: "Update the current user's profile fields. id must be the current user's id.",
98612
+ description: "Update the current user's profile fields: name, locale and default currency. id must be the current user's id. The login email and password are deliberately not settable here \u2014 account credentials are changed in the Splitwise app, not by an assistant.",
98613
+ annotations: { readOnlyHint: false, destructiveHint: true },
98614
+ // No `email` / `password`: changing the login email is an account
98615
+ // takeover primitive (a password reset to the new address follows), and
98616
+ // the only gate would be a model-set `confirm` that injected text from
98617
+ // comments, notifications or receipts could talk it into (fleet-audit
98618
+ // #250). z.object strips unknown keys, so a caller passing them anyway
98619
+ // has them dropped before the request is built.
98349
98620
  inputSchema: external_exports.object({
98350
98621
  id: external_exports.number().describe("User ID (must be the current user's id)"),
98351
98622
  first_name: external_exports.string().optional(),
98352
98623
  last_name: external_exports.string().optional(),
98353
- email: external_exports.string().optional(),
98354
- password: external_exports.string().optional(),
98355
98624
  locale: external_exports.string().optional(),
98356
98625
  default_currency: external_exports.string().optional(),
98357
98626
  confirm: schemaConfirm
98358
98627
  })
98359
98628
  },
98360
- async ({ id, first_name, last_name, email: email4, password, locale, default_currency, confirm }) => {
98629
+ async ({ id, first_name, last_name, locale, default_currency, confirm }) => {
98361
98630
  const body = pruneUndefined({
98362
98631
  first_name,
98363
98632
  last_name,
98364
- email: email4,
98365
- password,
98366
98633
  locale,
98367
98634
  default_currency
98368
98635
  });
98369
- const previewBody = { ...body, ...password !== void 0 ? { password: "[hidden]" } : {} };
98370
98636
  const gate = previewUnlessConfirmed(
98371
98637
  confirm,
98372
98638
  `Update current Splitwise user ${id} profile`,
98373
98639
  "POST",
98374
98640
  `/update_user/${id}`,
98375
- previewBody
98641
+ body
98376
98642
  );
98377
98643
  if (gate) return gate;
98378
98644
  const data = await client2.request("POST", `/update_user/${id}`, body);
@@ -99244,7 +99510,7 @@ function registerUtilityTools(server, client2) {
99244
99510
  // src/index.ts
99245
99511
  await runMcp({
99246
99512
  name: "splitwise-mcp",
99247
- version: "3.1.0",
99513
+ version: "3.1.2",
99248
99514
  // x-release-please-version
99249
99515
  deps: client,
99250
99516
  banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
package/dist/client.js CHANGED
@@ -59,6 +59,37 @@ function redactAssetQuery(err, search) {
59
59
  return err;
60
60
  return err instanceof ApiError ? new ApiError(err.status, message) : new Error(message);
61
61
  }
62
+ /** Flatten Splitwise's `errors` value (`{field: [msg]}`, `[msg]` or a string) into messages. */
63
+ function flattenErrors(errors) {
64
+ if (errors == null)
65
+ return [];
66
+ if (typeof errors === 'string')
67
+ return errors ? [errors] : [];
68
+ if (Array.isArray(errors))
69
+ return errors.flatMap((e) => flattenErrors(e));
70
+ if (typeof errors === 'object') {
71
+ return Object.entries(errors).flatMap(([field, value]) => flattenErrors(value).map((msg) => (field === 'base' ? msg : `${field}: ${msg}`)));
72
+ }
73
+ return [String(errors)];
74
+ }
75
+ /**
76
+ * Splitwise answers a failed write with HTTP 200, not an error status:
77
+ * create/update return `{expenses: [], errors: {...}}` and delete/undelete
78
+ * return `{success: false, errors: {...}}`. `fetchJson` only throws on
79
+ * non-2xx, so without this check a rejected write reaches the tool as an
80
+ * ordinary result and reads as "created"/"deleted". Throwing here makes every
81
+ * write tool report it with `isError`.
82
+ */
83
+ function assertWriteSucceeded(data) {
84
+ if (data === null || typeof data !== 'object' || Array.isArray(data))
85
+ return;
86
+ const record = data;
87
+ const messages = flattenErrors(record.errors);
88
+ if (messages.length === 0 && record.success !== false)
89
+ return;
90
+ const detail = messages.length > 0 ? messages.join('; ') : 'success: false with no error detail';
91
+ throw new Error(`Splitwise rejected the request: ${detail}`);
92
+ }
62
93
  export class SplitwiseClient {
63
94
  apiKey;
64
95
  /** Which source supplied the API key — a LABEL, never the value. */
@@ -138,7 +169,10 @@ export class SplitwiseClient {
138
169
  return this.apiKey;
139
170
  }
140
171
  async request(method, path, body) {
141
- return this.api.fetchJson(method, path, body !== undefined ? { body } : {});
172
+ const data = await this.api.fetchJson(method, path, body !== undefined ? { body } : {});
173
+ if (method.toUpperCase() !== 'GET')
174
+ assertWriteSucceeded(data);
175
+ return data;
142
176
  }
143
177
  /**
144
178
  * Fetch the raw bytes behind an absolute asset URL the API handed back (an
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { registerUtilityTools } from './tools/utilities.js';
19
19
  // client into the same registrars instead of this singleton.
20
20
  await runMcp({
21
21
  name: 'splitwise-mcp',
22
- version: '3.1.0', // x-release-please-version
22
+ version: '3.1.2', // x-release-please-version
23
23
  deps: client,
24
24
  banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
25
25
  tools: [
@@ -25,29 +25,30 @@ export function registerUserTools(server, client) {
25
25
  return minifiedResult(viewUser(resolveView(view, SW_VIEWS), data));
26
26
  });
27
27
  server.registerTool('sw_update_user', {
28
- description: "Update the current user's profile fields. id must be the current user's id.",
28
+ description: "Update the current user's profile fields: name, locale and default currency. id must be the current user's id. The login email and password are deliberately not settable here — account credentials are changed in the Splitwise app, not by an assistant.",
29
+ annotations: { readOnlyHint: false, destructiveHint: true },
30
+ // No `email` / `password`: changing the login email is an account
31
+ // takeover primitive (a password reset to the new address follows), and
32
+ // the only gate would be a model-set `confirm` that injected text from
33
+ // comments, notifications or receipts could talk it into (fleet-audit
34
+ // #250). z.object strips unknown keys, so a caller passing them anyway
35
+ // has them dropped before the request is built.
29
36
  inputSchema: z.object({
30
37
  id: z.number().describe("User ID (must be the current user's id)"),
31
38
  first_name: z.string().optional(),
32
39
  last_name: z.string().optional(),
33
- email: z.string().optional(),
34
- password: z.string().optional(),
35
40
  locale: z.string().optional(),
36
41
  default_currency: z.string().optional(),
37
42
  confirm: schemaConfirm,
38
43
  }),
39
- }, async ({ id, first_name, last_name, email, password, locale, default_currency, confirm }) => {
44
+ }, async ({ id, first_name, last_name, locale, default_currency, confirm }) => {
40
45
  const body = pruneUndefined({
41
46
  first_name,
42
47
  last_name,
43
- email,
44
- password,
45
48
  locale,
46
49
  default_currency,
47
50
  });
48
- // Never echo the password in the dry-run preview.
49
- const previewBody = { ...body, ...(password !== undefined ? { password: '[hidden]' } : {}) };
50
- const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, 'POST', `/update_user/${id}`, previewBody);
51
+ const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, 'POST', `/update_user/${id}`, body);
51
52
  if (gate)
52
53
  return gate;
53
54
  const data = await client.request('POST', `/update_user/${id}`, body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
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>",
@@ -45,11 +45,11 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^1.0.0",
48
+ "@chrischall/mcp-utils": "^2.4.0",
49
49
  "@modelcontextprotocol/server": "^2.0.0",
50
50
  "dotenv": "^17.4.0",
51
51
  "unpdf": "^1.8.1",
52
- "zod": "^4.6.2"
52
+ "zod": "^4.6.5"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@modelcontextprotocol/client": "^2.0.0",
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": "3.1.0",
9
+ "version": "3.1.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "splitwise-mcp",
14
- "version": "3.1.0",
14
+ "version": "3.1.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },