splitwise-mcp 3.3.1 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -92,6 +92,7 @@ var require_util = __commonJS({
92
92
  exports.explicitlyAborted = explicitlyAborted2;
93
93
  exports.prefixIssues = prefixIssues2;
94
94
  exports.unwrapMessage = unwrapMessage2;
95
+ exports.sizeCaps = sizeCaps2;
95
96
  exports.attachSchema = attachSchema2;
96
97
  exports.finalizeIssue = finalizeIssue2;
97
98
  exports.getSizableOrigin = getSizableOrigin2;
@@ -626,6 +627,18 @@ var require_util = __commonJS({
626
627
  function unwrapMessage2(message) {
627
628
  return typeof message === "string" ? message : message?.message;
628
629
  }
630
+ function sizeCaps2(defChecks) {
631
+ const caps = [];
632
+ for (const ch2 of defChecks ?? []) {
633
+ const d2 = ch2._zod.def;
634
+ if (!/_(length|size)$|_equals$/.test(d2.check))
635
+ break;
636
+ const max = d2.maximum ?? d2.length ?? d2.size;
637
+ if (max >= 0)
638
+ caps.push([max, ch2]);
639
+ }
640
+ return caps;
641
+ }
629
642
  function attachSchema2(issues, start, inst) {
630
643
  var _a4;
631
644
  for (let i2 = start; i2 < issues.length; i2++) {
@@ -1948,10 +1961,10 @@ var require_checks = __commonJS({
1948
1961
  const size = input2.size;
1949
1962
  if (size === def.size)
1950
1963
  return;
1951
- const tooBig = size > def.size;
1964
+ const tooBig2 = size > def.size;
1952
1965
  payload.issues.push({
1953
1966
  origin: util.getSizableOrigin(input2),
1954
- ...tooBig ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
1967
+ ...tooBig2 ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
1955
1968
  inclusive: true,
1956
1969
  exact: true,
1957
1970
  input: payload.value,
@@ -2015,10 +2028,10 @@ var require_checks = __commonJS({
2015
2028
  if (length === def.length)
2016
2029
  return;
2017
2030
  const origin = util.getLengthableOrigin(input2);
2018
- const tooBig = length > def.length;
2031
+ const tooBig2 = length > def.length;
2019
2032
  payload.issues.push({
2020
2033
  origin,
2021
- ...tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
2034
+ ...tooBig2 ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
2022
2035
  inclusive: true,
2023
2036
  exact: true,
2024
2037
  input: payload.value,
@@ -2290,7 +2303,7 @@ var require_versions = __commonJS({
2290
2303
  exports.version = {
2291
2304
  major: 4,
2292
2305
  minor: 6,
2293
- patch: 5
2306
+ patch: 7
2294
2307
  };
2295
2308
  (function() {
2296
2309
  var keys = Object.getOwnPropertyNames(exports);
@@ -3124,6 +3137,31 @@ var require_schemas = __commonJS({
3124
3137
  return payload;
3125
3138
  };
3126
3139
  });
3140
+ function capSize2(inst, size) {
3141
+ const caps = util.sizeCaps(inst._zod.def.checks);
3142
+ if (!caps.length)
3143
+ return;
3144
+ const cap = Math.min(...caps.map((c2) => c2[0]));
3145
+ const parse3 = inst._zod.parse;
3146
+ inst._zod.parse = (payload, ctx) => {
3147
+ const n2 = size(payload.value);
3148
+ if (n2 > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
3149
+ for (const [max, ch2] of util.sizeCaps(inst._zod.def.checks)) {
3150
+ if (n2 > max && ch2._zod.def.when(payload)) {
3151
+ const start = payload.issues.length;
3152
+ ch2._zod.check(payload);
3153
+ payload.issues[start].continue = false;
3154
+ util.attachSchema(payload.issues, start, inst);
3155
+ return payload;
3156
+ }
3157
+ }
3158
+ }
3159
+ return parse3(payload, ctx);
3160
+ };
3161
+ }
3162
+ var arraySize2 = (input2) => Array.isArray(input2) ? input2.length : -1;
3163
+ var mapSize2 = (input2) => input2 instanceof Map ? input2.size : -1;
3164
+ var setSize2 = (input2) => input2 instanceof Set ? input2.size : -1;
3127
3165
  function handleArrayResult2(result, final, index) {
3128
3166
  if (result.issues.length) {
3129
3167
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -3167,6 +3205,7 @@ var require_schemas = __commonJS({
3167
3205
  }
3168
3206
  return payload;
3169
3207
  };
3208
+ capSize2(inst, arraySize2);
3170
3209
  });
3171
3210
  function handlePropertyResult2(result, final, key, input2, optin, optout) {
3172
3211
  const isPresent = key in input2;
@@ -4123,6 +4162,7 @@ var require_schemas = __commonJS({
4123
4162
  return Promise.all(proms).then(() => payload);
4124
4163
  return payload;
4125
4164
  };
4165
+ capSize2(inst, mapSize2);
4126
4166
  });
4127
4167
  function handleMapResult2(keyResult, valueResult, final, key, input2, inst, ctx) {
4128
4168
  if (keyResult.issues.length) {
@@ -4189,6 +4229,7 @@ var require_schemas = __commonJS({
4189
4229
  return Promise.all(proms).then(() => payload);
4190
4230
  return payload;
4191
4231
  };
4232
+ capSize2(inst, setSize2);
4192
4233
  });
4193
4234
  function handleSetResult2(result, final) {
4194
4235
  if (result.issues.length) {
@@ -16293,9 +16334,15 @@ ${code2}
16293
16334
  doc.write(`}`);
16294
16335
  return outputVar;
16295
16336
  }
16337
+ function generateSizeGate2(doc, schema, size) {
16338
+ for (const [max, check2] of util.sizeCaps(schema._zod.def.checks)) {
16339
+ doc.write(`if (${size} > ${numericOperand2(max, check2._zod.def.check)}) return INVALID;`);
16340
+ }
16341
+ }
16296
16342
  function generateArrayCheck2(doc, ctx, schema, accessor, buildsValue = true) {
16297
16343
  const def = schema._zod.def;
16298
16344
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
16345
+ generateSizeGate2(doc, schema, `${accessor}.length`);
16299
16346
  const outputVar = buildsValue ? newVar2(ctx) : null;
16300
16347
  const iVar = newVar2(ctx);
16301
16348
  const elemVar = newVar2(ctx);
@@ -16695,6 +16742,7 @@ ${code2}
16695
16742
  function generateMapCheck2(doc, ctx, schema, accessor) {
16696
16743
  const def = schema._zod.def;
16697
16744
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
16745
+ generateSizeGate2(doc, schema, `${accessor}.size`);
16698
16746
  const outputVar = newVar2(ctx);
16699
16747
  const kVar = newVar2(ctx);
16700
16748
  const valVar = newVar2(ctx);
@@ -16711,6 +16759,7 @@ ${code2}
16711
16759
  function generateSetCheck2(doc, ctx, schema, accessor) {
16712
16760
  const def = schema._zod.def;
16713
16761
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
16762
+ generateSizeGate2(doc, schema, `${accessor}.size`);
16714
16763
  const outputVar = newVar2(ctx);
16715
16764
  const valVar = newVar2(ctx);
16716
16765
  doc.write(`const ${outputVar} = new Set();`);
@@ -63308,7 +63357,7 @@ function declaredDialect(schema, remedy) {
63308
63357
  throw new Error(`JSON Schema declares an unsupported dialect ("$schema": "${schema.$schema.slice(0, 200)}"). The default validator supports JSON Schema 2020-12, 2019-09, draft-07, and draft-06; ${remedy}`);
63309
63358
  }
63310
63359
 
63311
- // node_modules/@modelcontextprotocol/core/dist/auth-BNDyLTqp.mjs
63360
+ // node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/dist/auth-BNDyLTqp.mjs
63312
63361
  var z = __toESM(require_v4(), 1);
63313
63362
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
63314
63363
  var SUPPORTED_PROTOCOL_VERSIONS = [
@@ -78172,7 +78221,7 @@ var API_KEY_RE = new RegExp([
78172
78221
  // webhook signing secret (Stripe-style)
78173
78222
  ].map((p2) => `\\b${p2}`).join("|"), "g");
78174
78223
  var GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
78175
- var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
78224
+ var QUERY_SECRET_RE = new RegExp(`([?&#;](?:(?:access|refresh|id|auth|session|csrf|xsrf|api)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|secret|token|auth|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
78176
78225
  var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
78177
78226
  var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
78178
78227
  var HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
@@ -78183,6 +78232,8 @@ var JSON_SECRET_KEYS = [
78183
78232
  "api[_-]?secret",
78184
78233
  "(?:x[_-])?api[_-]?key",
78185
78234
  "private[_-]?key",
78235
+ "aws[_-]?secret[_-]?access[_-]?key",
78236
+ // fleet-audit#1297
78186
78237
  "code[_-]?verifier",
78187
78238
  // PKCE; a bare `verifier` key is too generic for JSON
78188
78239
  `x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
@@ -78880,6 +78931,7 @@ __export(util_exports, {
78880
78931
  required: () => required,
78881
78932
  safeExtend: () => safeExtend,
78882
78933
  shallowClone: () => shallowClone,
78934
+ sizeCaps: () => sizeCaps,
78883
78935
  slugify: () => slugify,
78884
78936
  stringifyPrimitive: () => stringifyPrimitive,
78885
78937
  toZod: () => toZod,
@@ -79399,6 +79451,18 @@ function prefixIssues(path, issues) {
79399
79451
  function unwrapMessage(message) {
79400
79452
  return typeof message === "string" ? message : message?.message;
79401
79453
  }
79454
+ function sizeCaps(defChecks) {
79455
+ const caps = [];
79456
+ for (const ch2 of defChecks ?? []) {
79457
+ const d2 = ch2._zod.def;
79458
+ if (!/_(length|size)$|_equals$/.test(d2.check))
79459
+ break;
79460
+ const max = d2.maximum ?? d2.length ?? d2.size;
79461
+ if (max >= 0)
79462
+ caps.push([max, ch2]);
79463
+ }
79464
+ return caps;
79465
+ }
79402
79466
  function attachSchema(issues, start, inst) {
79403
79467
  var _a4;
79404
79468
  for (let i2 = start; i2 < issues.length; i2++) {
@@ -80524,10 +80588,10 @@ var $ZodCheckSizeEquals = /* @__PURE__ */ $constructor("$ZodCheckSizeEquals", (i
80524
80588
  const size = input2.size;
80525
80589
  if (size === def.size)
80526
80590
  return;
80527
- const tooBig = size > def.size;
80591
+ const tooBig2 = size > def.size;
80528
80592
  payload.issues.push({
80529
80593
  origin: getSizableOrigin(input2),
80530
- ...tooBig ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
80594
+ ...tooBig2 ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
80531
80595
  inclusive: true,
80532
80596
  exact: true,
80533
80597
  input: payload.value,
@@ -80591,10 +80655,10 @@ var $ZodCheckLengthEquals = /* @__PURE__ */ $constructor("$ZodCheckLengthEquals"
80591
80655
  if (length === def.length)
80592
80656
  return;
80593
80657
  const origin = getLengthableOrigin(input2);
80594
- const tooBig = length > def.length;
80658
+ const tooBig2 = length > def.length;
80595
80659
  payload.issues.push({
80596
80660
  origin,
80597
- ...tooBig ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
80661
+ ...tooBig2 ? { code: "too_big", maximum: def.length } : { code: "too_small", minimum: def.length },
80598
80662
  inclusive: true,
80599
80663
  exact: true,
80600
80664
  input: payload.value,
@@ -80819,7 +80883,7 @@ ${content.join("\n")}
80819
80883
  var version = {
80820
80884
  major: 4,
80821
80885
  minor: 6,
80822
- patch: 5
80886
+ patch: 7
80823
80887
  };
80824
80888
 
80825
80889
  // node_modules/zod/v4/core/schemas.js
@@ -81575,6 +81639,31 @@ var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
81575
81639
  return payload;
81576
81640
  };
81577
81641
  });
81642
+ function capSize(inst, size) {
81643
+ const caps = sizeCaps(inst._zod.def.checks);
81644
+ if (!caps.length)
81645
+ return;
81646
+ const cap = Math.min(...caps.map((c2) => c2[0]));
81647
+ const parse3 = inst._zod.parse;
81648
+ inst._zod.parse = (payload, ctx) => {
81649
+ const n2 = size(payload.value);
81650
+ if (n2 > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
81651
+ for (const [max, ch2] of sizeCaps(inst._zod.def.checks)) {
81652
+ if (n2 > max && ch2._zod.def.when(payload)) {
81653
+ const start = payload.issues.length;
81654
+ ch2._zod.check(payload);
81655
+ payload.issues[start].continue = false;
81656
+ attachSchema(payload.issues, start, inst);
81657
+ return payload;
81658
+ }
81659
+ }
81660
+ }
81661
+ return parse3(payload, ctx);
81662
+ };
81663
+ }
81664
+ var arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
81665
+ var mapSize = (input2) => input2 instanceof Map ? input2.size : -1;
81666
+ var setSize = (input2) => input2 instanceof Set ? input2.size : -1;
81578
81667
  function handleArrayResult(result, final, index) {
81579
81668
  if (result.issues.length) {
81580
81669
  final.issues.push(...prefixIssues(index, result.issues));
@@ -81618,6 +81707,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
81618
81707
  }
81619
81708
  return payload;
81620
81709
  };
81710
+ capSize(inst, arraySize);
81621
81711
  });
81622
81712
  function handlePropertyResult(result, final, key, input2, optin, optout) {
81623
81713
  const isPresent = key in input2;
@@ -82574,6 +82664,7 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
82574
82664
  return Promise.all(proms).then(() => payload);
82575
82665
  return payload;
82576
82666
  };
82667
+ capSize(inst, mapSize);
82577
82668
  });
82578
82669
  function handleMapResult(keyResult, valueResult, final, key, input2, inst, ctx) {
82579
82670
  if (keyResult.issues.length) {
@@ -82640,6 +82731,7 @@ var $ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
82640
82731
  return Promise.all(proms).then(() => payload);
82641
82732
  return payload;
82642
82733
  };
82734
+ capSize(inst, setSize);
82643
82735
  });
82644
82736
  function handleSetResult(result, final) {
82645
82737
  if (result.issues.length) {
@@ -92170,9 +92262,15 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
92170
92262
  doc.write(`}`);
92171
92263
  return outputVar;
92172
92264
  }
92265
+ function generateSizeGate(doc, schema, size) {
92266
+ for (const [max, check2] of sizeCaps(schema._zod.def.checks)) {
92267
+ doc.write(`if (${size} > ${numericOperand(max, check2._zod.def.check)}) return INVALID;`);
92268
+ }
92269
+ }
92173
92270
  function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
92174
92271
  const def = schema._zod.def;
92175
92272
  doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
92273
+ generateSizeGate(doc, schema, `${accessor}.length`);
92176
92274
  const outputVar = buildsValue ? newVar(ctx) : null;
92177
92275
  const iVar = newVar(ctx);
92178
92276
  const elemVar = newVar(ctx);
@@ -92572,6 +92670,7 @@ function literalPropertyKey(ctx, key) {
92572
92670
  function generateMapCheck(doc, ctx, schema, accessor) {
92573
92671
  const def = schema._zod.def;
92574
92672
  doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
92673
+ generateSizeGate(doc, schema, `${accessor}.size`);
92575
92674
  const outputVar = newVar(ctx);
92576
92675
  const kVar = newVar(ctx);
92577
92676
  const valVar = newVar(ctx);
@@ -92588,6 +92687,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
92588
92687
  function generateSetCheck(doc, ctx, schema, accessor) {
92589
92688
  const def = schema._zod.def;
92590
92689
  doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
92690
+ generateSizeGate(doc, schema, `${accessor}.size`);
92591
92691
  const outputVar = newVar(ctx);
92592
92692
  const valVar = newVar(ctx);
92593
92693
  doc.write(`const ${outputVar} = new Set();`);
@@ -98900,11 +99000,21 @@ function sanitizeBaseName(base) {
98900
99000
  const safe = base.replace(/[/\\]+/g, "_").replace(/\.\.+/g, "_").replace(/^\.+/, "").trim();
98901
99001
  return safe.length > 0 ? safe : "file";
98902
99002
  }
99003
+ var SAFE_EXTENSION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/;
99004
+ function isSafeExtension(ext) {
99005
+ return SAFE_EXTENSION.test(ext);
99006
+ }
99007
+ function assertSafeExtension(ext, caller) {
99008
+ if (!isSafeExtension(ext)) {
99009
+ throw new TypeError(`${caller}: extension must be 1-16 characters of A-Z a-z 0-9 . _ - starting with a letter or digit, got ${JSON.stringify(ext)}.`);
99010
+ }
99011
+ }
98903
99012
  function candidate(dir, safeBase, ext, n2) {
98904
99013
  return join4(dir, n2 === 1 ? `${safeBase}.${ext}` : `${safeBase}-${n2}.${ext}`);
98905
99014
  }
98906
99015
  var DEFAULT_MAX_ATTEMPTS = 1e3;
98907
99016
  function writeUniqueFileSync(dir, baseName, ext, bytes, mode) {
99017
+ assertSafeExtension(ext, "writeBinaryOutput");
98908
99018
  mkdirSync2(dir, { recursive: true });
98909
99019
  const safe = sanitizeBaseName(baseName);
98910
99020
  for (let n2 = 1; ; n2++) {
@@ -98959,19 +99069,61 @@ function defaultOutputDir(envVar, name) {
98959
99069
  var MIME_EXT = {
98960
99070
  "image/png": "png",
98961
99071
  "image/jpeg": "jpg",
99072
+ "image/jpg": "jpg",
99073
+ "image/pjpeg": "jpg",
98962
99074
  "image/webp": "webp",
98963
99075
  "image/gif": "gif",
98964
99076
  "image/heic": "heic",
99077
+ "image/heif": "heif",
98965
99078
  "image/avif": "avif",
99079
+ "image/tiff": "tif",
99080
+ "image/bmp": "bmp",
99081
+ "image/svg+xml": "svg",
98966
99082
  "application/pdf": "pdf",
98967
99083
  "application/json": "json",
98968
99084
  "application/zip": "zip",
99085
+ "application/gzip": "gz",
99086
+ "text/plain": "txt",
99087
+ "text/csv": "csv",
99088
+ "text/html": "html",
99089
+ "text/calendar": "ics",
98969
99090
  "audio/midi": "mid",
99091
+ "audio/mpeg": "mp3",
99092
+ "audio/mp3": "mp3",
99093
+ "audio/wav": "wav",
99094
+ "audio/wave": "wav",
99095
+ "audio/x-wav": "wav",
99096
+ "audio/ogg": "ogg",
99097
+ "audio/webm": "webm",
99098
+ "audio/mp4": "m4a",
98970
99099
  "video/mp4": "mp4",
98971
- "video/quicktime": "mov"
99100
+ "video/quicktime": "mov",
99101
+ "video/webm": "webm"
98972
99102
  };
99103
+ var PLAIN_EXT = /^[a-z0-9]{1,5}$/;
99104
+ function extensionForMime(mime, opts = {}) {
99105
+ const fallback = opts.fallback ?? "bin";
99106
+ assertSafeExtension(fallback, "extensionForMime");
99107
+ const type = (mime ?? "").split(";", 1)[0].trim().toLowerCase();
99108
+ const known = MIME_EXT[type];
99109
+ if (known !== void 0)
99110
+ return known;
99111
+ const slash = type.indexOf("/");
99112
+ const subtype = slash > 0 && type.indexOf("/", slash + 1) < 0 ? type.slice(slash + 1).replace(/^x-/, "") : "";
99113
+ if (PLAIN_EXT.test(subtype))
99114
+ return subtype;
99115
+ if (opts.url !== void 0) {
99116
+ try {
99117
+ const fromUrl = /\.([a-z0-9]{1,5})$/i.exec(new URL(opts.url).pathname)?.[1]?.toLowerCase();
99118
+ if (fromUrl !== void 0 && isSafeExtension(fromUrl))
99119
+ return fromUrl;
99120
+ } catch {
99121
+ }
99122
+ }
99123
+ return fallback;
99124
+ }
98973
99125
  function writeBinaryOutput(opts) {
98974
- const ext = opts.extension ?? (opts.mimeType ? MIME_EXT[opts.mimeType] : void 0) ?? "bin";
99126
+ const ext = opts.extension ?? extensionForMime(opts.mimeType);
98975
99127
  return writeUniqueFileSync(opts.dir, opts.baseName, ext, Buffer.from(opts.base64, "base64"), opts.mode);
98976
99128
  }
98977
99129
 
@@ -99040,10 +99192,34 @@ function getHeader(headers, name) {
99040
99192
  return typeof value === "string" ? value : null;
99041
99193
  }
99042
99194
 
99195
+ // node_modules/@chrischall/mcp-utils/dist/internal/ascii.js
99196
+ function asciiLower(s2) {
99197
+ return s2.replace(/[A-Z]+/g, (m2) => m2.toLowerCase());
99198
+ }
99199
+
99043
99200
  // node_modules/@chrischall/mcp-utils/dist/scrape/index.js
99044
99201
  var NEXT_DATA_MAX_CHARS = 16 * 1024 * 1024;
99045
99202
  function isCloudflareChallenge(html) {
99046
- return /_cf_chl_opt/.test(html) || /<title[^>]*>\s*Just a moment/i.test(html);
99203
+ return html.includes("_cf_chl_opt") || hasJustAMomentTitle(html);
99204
+ }
99205
+ var JUST_A_MOMENT = "just a moment";
99206
+ function hasJustAMomentTitle(html) {
99207
+ const lower = asciiLower(html);
99208
+ let i2 = 0;
99209
+ for (; ; ) {
99210
+ const open2 = lower.indexOf("<title", i2);
99211
+ if (open2 < 0)
99212
+ return false;
99213
+ const gt2 = lower.indexOf(">", open2 + 6);
99214
+ if (gt2 < 0)
99215
+ return false;
99216
+ let j2 = gt2 + 1;
99217
+ while (j2 < lower.length && /\s/.test(lower[j2]))
99218
+ j2++;
99219
+ if (lower.startsWith(JUST_A_MOMENT, j2))
99220
+ return true;
99221
+ i2 = gt2 + 1;
99222
+ }
99047
99223
  }
99048
99224
 
99049
99225
  // node_modules/@chrischall/mcp-utils/dist/http/net-atoms.js
@@ -99072,7 +99248,7 @@ var ResponseTooLargeError = class extends McpToolError {
99072
99248
  };
99073
99249
  async function readBytesCapped(res, maxBytes, service, bounded) {
99074
99250
  if (maxBytes !== void 0) {
99075
- const declared = Number(res.headers.get("content-length") ?? NaN);
99251
+ const declared = Number(responseHeader(res, "content-length") ?? NaN);
99076
99252
  if (Number.isFinite(declared) && declared > maxBytes) {
99077
99253
  cancelStream(res.body);
99078
99254
  throw new ResponseTooLargeError(service, maxBytes);
@@ -99162,6 +99338,29 @@ var DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
99162
99338
  var defaultSleep = (ms2) => new Promise((r2) => setTimeout(r2, ms2));
99163
99339
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
99164
99340
  var DEFAULT_MAX_REDIRECTS = 5;
99341
+ var FETCH_MAX_REDIRECTS = 20;
99342
+ var CROSS_ORIGIN_DROPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
99343
+ function withoutCredentials(headers, tokenHeader) {
99344
+ const drop = new Set(CROSS_ORIGIN_DROPPED_HEADERS);
99345
+ if (tokenHeader !== void 0)
99346
+ drop.add(tokenHeader.toLowerCase());
99347
+ const out = {};
99348
+ for (const [k2, v2] of Object.entries(headers))
99349
+ if (!drop.has(k2.toLowerCase()))
99350
+ out[k2] = v2;
99351
+ return out;
99352
+ }
99353
+ function joinQuery(path, query) {
99354
+ if (!query)
99355
+ return path;
99356
+ const hashAt = path.indexOf("#");
99357
+ const head = hashAt < 0 ? path : path.slice(0, hashAt);
99358
+ const fragment = hashAt < 0 ? "" : path.slice(hashAt);
99359
+ if (!head.includes("?"))
99360
+ return `${head}${query}${fragment}`;
99361
+ const sep2 = head.endsWith("?") || head.endsWith("&") ? "" : "&";
99362
+ return `${head}${sep2}${query.slice(1)}${fragment}`;
99363
+ }
99165
99364
  var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
99166
99365
  function cancelBody(res) {
99167
99366
  try {
@@ -99401,7 +99600,9 @@ function createApiClient(opts) {
99401
99600
  const retryStatuses = retry.statuses ?? [429];
99402
99601
  const timeoutMs = opts.timeout === false ? 0 : opts.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
99403
99602
  const redirectMode = opts.redirect;
99404
- const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
99603
+ const stripCrossOrigin = redirectMode === void 0 && opts.tokenHeader !== void 0;
99604
+ const manualRedirects = redirectMode === "same-origin" || stripCrossOrigin;
99605
+ const maxRedirects = opts.maxRedirects ?? (stripCrossOrigin ? FETCH_MAX_REDIRECTS : DEFAULT_MAX_REDIRECTS);
99405
99606
  const reportWriteOutcome = opts.writeOutcomeUnknown !== false;
99406
99607
  const defaultMaxBytes = checkMaxBytes("maxResponseBytes", opts.maxResponseBytes);
99407
99608
  if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0) {
@@ -99503,7 +99704,7 @@ function createApiClient(opts) {
99503
99704
  `Refusing request to ${service}: path ${JSON.stringify(path.split(/[?#]/, 1)[0])} contains a ${hazard}, which URL normalisation would rewrite. Build paths with apiPath\`...\` so each value is one encoded segment.`
99504
99705
  );
99505
99706
  }
99506
- const url4 = `${base}${path}${query}`;
99707
+ const url4 = `${base}${joinQuery(path, query)}`;
99507
99708
  if (baseOrigin === void 0)
99508
99709
  return url4;
99509
99710
  let parsed;
@@ -99517,14 +99718,21 @@ function createApiClient(opts) {
99517
99718
  }
99518
99719
  return url4;
99519
99720
  }
99520
- function sameOriginHop(location, from) {
99521
- let next;
99721
+ function resolveLocation(location, from) {
99522
99722
  try {
99523
- next = new URL(location, from);
99723
+ return new URL(location, from).href;
99524
99724
  } catch {
99525
99725
  throw new RedirectRefusedError(`Refusing redirect from ${service}: the Location header is not a valid URL.`);
99526
99726
  }
99527
- if (baseOrigin === void 0 || next.origin !== baseOrigin || next.username !== baseParts?.username || next.password !== baseParts?.password) {
99727
+ }
99728
+ function onBaseOrigin(href) {
99729
+ const next = new URL(href);
99730
+ return baseOrigin !== void 0 && next.origin === baseOrigin && next.username === baseParts?.username && next.password === baseParts?.password;
99731
+ }
99732
+ function sameOriginHop(location, from) {
99733
+ const href = resolveLocation(location, from);
99734
+ const next = new URL(href);
99735
+ if (!onBaseOrigin(href)) {
99528
99736
  throw new RedirectRefusedError(`Refusing redirect from ${service} to ${next.origin}: it leaves the base origin ${baseOrigin ?? "(unparseable)"}, and following it would re-send the credential there.`);
99529
99737
  }
99530
99738
  return next.href;
@@ -99534,20 +99742,21 @@ function createApiClient(opts) {
99534
99742
  const query = opt.query ? buildQueryString(opt.query) : "";
99535
99743
  const url4 = resolveUrl(path, query);
99536
99744
  const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
99537
- const fetchHop = (hopUrl, hopMethod, withBody, token, signal) => {
99745
+ const fetchHop = (hopUrl, hopMethod, withBody, token, signal, credentialed = true) => {
99538
99746
  if (!signal?.aborted)
99539
99747
  dispatch.sent = true;
99748
+ const headers = {
99749
+ Accept: "application/json",
99750
+ ...withBody && contentType !== void 0 ? { "Content-Type": contentType } : {},
99751
+ ...opts.baseHeaders,
99752
+ ...authHeader(token || void 0),
99753
+ ...opt.headers
99754
+ };
99540
99755
  return doFetch(hopUrl, {
99541
99756
  method: hopMethod,
99542
- headers: {
99543
- Accept: "application/json",
99544
- ...withBody && contentType !== void 0 ? { "Content-Type": contentType } : {},
99545
- ...opts.baseHeaders,
99546
- ...authHeader(token || void 0),
99547
- ...opt.headers
99548
- },
99757
+ headers: credentialed ? headers : withoutCredentials(headers, opts.tokenHeader),
99549
99758
  ...signal ? { signal } : {},
99550
- ...redirectMode !== void 0 ? { redirect: redirectMode === "same-origin" ? "manual" : redirectMode } : {},
99759
+ ...manualRedirects ? { redirect: "manual" } : redirectMode !== void 0 ? { redirect: redirectMode } : {},
99551
99760
  ...withBody ? bodyInit : {}
99552
99761
  }).catch((err) => {
99553
99762
  markTransportFailure(err);
@@ -99555,13 +99764,14 @@ function createApiClient(opts) {
99555
99764
  });
99556
99765
  };
99557
99766
  const fetchWith = async (token, signal) => {
99558
- if (redirectMode !== "same-origin")
99767
+ if (!manualRedirects)
99559
99768
  return fetchHop(url4, method, true, token, signal);
99560
99769
  let hopUrl = url4;
99561
99770
  let hopMethod = method;
99562
99771
  let withBody = true;
99772
+ let credentialed = true;
99563
99773
  for (let hops = 0; ; hops += 1) {
99564
- const res = await fetchHop(hopUrl, hopMethod, withBody, token, signal);
99774
+ const res = await fetchHop(hopUrl, hopMethod, withBody, token, signal, credentialed);
99565
99775
  const location = REDIRECT_STATUSES.has(res.status) ? responseHeader(res, "location") : void 0;
99566
99776
  if (location === void 0 || location === null)
99567
99777
  return res;
@@ -99569,7 +99779,13 @@ function createApiClient(opts) {
99569
99779
  if (hops >= maxRedirects) {
99570
99780
  throw new RedirectRefusedError(`Refusing request to ${service}: too many redirects (more than ${maxRedirects}).`);
99571
99781
  }
99572
- hopUrl = sameOriginHop(location, hopUrl);
99782
+ if (stripCrossOrigin) {
99783
+ hopUrl = resolveLocation(location, hopUrl);
99784
+ if (credentialed && !onBaseOrigin(hopUrl))
99785
+ credentialed = false;
99786
+ } else {
99787
+ hopUrl = sameOriginHop(location, hopUrl);
99788
+ }
99573
99789
  if (res.status === 303 || (res.status === 301 || res.status === 302) && hopMethod === "POST") {
99574
99790
  if (hopMethod !== "HEAD")
99575
99791
  hopMethod = "GET";
@@ -99842,7 +100058,9 @@ var SplitwiseClient = class {
99842
100058
  * Optional constructor seam: a hosted per-user deployment builds one client
99843
100059
  * per request with that user's `apiKey` injected. The stdio path passes no
99844
100060
  * options, so the key resolves from the environment exactly as before —
99845
- * byte-for-byte identical behaviour.
100061
+ * byte-for-byte identical behaviour. `baseUrl` points the API at another
100062
+ * origin (the HTTP integration tests' fake Splitwise); it defaults to the
100063
+ * real API.
99846
100064
  */
99847
100065
  constructor(opts) {
99848
100066
  const injected = opts?.apiKey;
@@ -99855,7 +100073,7 @@ var SplitwiseClient = class {
99855
100073
  this.apiKey = key;
99856
100074
  this.configError = null;
99857
100075
  }
99858
- this.api = this.buildApiClient(BASE_URL, true);
100076
+ this.api = this.buildApiClient(opts?.baseUrl ?? BASE_URL, true);
99859
100077
  }
99860
100078
  /**
99861
100079
  * Which source supplied the API key, for `sw_healthcheck`. Returns a LABEL
@@ -99895,6 +100113,16 @@ var SplitwiseClient = class {
99895
100113
  if (method.toUpperCase() !== "GET") assertWriteSucceeded(data);
99896
100114
  return data;
99897
100115
  }
100116
+ /**
100117
+ * A write sent as multipart/form-data — the only encoding Splitwise accepts
100118
+ * for an expense that carries a receipt file. Same auth, retry and
100119
+ * write-rejection handling as `request`.
100120
+ */
100121
+ async requestMultipart(method, path, formData) {
100122
+ const data = await this.api.fetchJson(method, path, { formData });
100123
+ assertWriteSucceeded(data);
100124
+ return data;
100125
+ }
99898
100126
  /**
99899
100127
  * Fetch the raw bytes behind an absolute asset URL the API handed back (an
99900
100128
  * expense receipt). Splitwise serves receipts two ways and this covers both:
@@ -100617,6 +100845,129 @@ function swUntrustedEnvelope(payload) {
100617
100845
  return untrustedEnvelope(payload, { note: SW_UNTRUSTED_NOTE });
100618
100846
  }
100619
100847
 
100848
+ // src/tools/_receipt.ts
100849
+ import { createHash as createHash4 } from "crypto";
100850
+ var MAX_RECEIPT_BYTES = 10 * 1024 * 1024;
100851
+ var LIMIT_LABEL = "10 MiB";
100852
+ var RECEIPT_TYPES = [
100853
+ { mime: "image/png", ext: "png", magic: [137, 80, 78, 71, 13, 10, 26, 10] },
100854
+ { mime: "image/jpeg", ext: "jpg", magic: [255, 216, 255] },
100855
+ { mime: "image/gif", ext: "gif", magic: [71, 73, 70, 56] },
100856
+ { mime: "application/pdf", ext: "pdf", magic: [37, 80, 68, 70, 45] }
100857
+ ];
100858
+ var MIME_ALIASES = { "image/jpg": "image/jpeg", "image/pjpeg": "image/jpeg" };
100859
+ var receiptSchema = external_exports.object({
100860
+ base64: external_exports.string().min(1).describe("The file bytes, base64-encoded (a data: URL prefix is accepted and stripped). The normal way to pass a receipt.").optional(),
100861
+ path: external_exports.string().min(1).describe(
100862
+ "A file inside SPLITWISE_INPUT_DIR on the machine running this server (relative to it, or absolute). Only works when that variable is set \u2014 never on a hosted server; use base64 there."
100863
+ ).optional(),
100864
+ filename: external_exports.string().min(1).max(200).describe('Name to store the file under, e.g. "receipt.pdf".'),
100865
+ mime_type: external_exports.string().describe("Optional. If given, it must match the type detected from the file contents.").optional()
100866
+ }).describe(
100867
+ `Receipt to attach: PNG, JPEG, GIF or PDF, up to ${LIMIT_LABEL}. Give exactly one of base64 or path. The type is detected from the file contents. Replaces any receipt already on the expense.`
100868
+ );
100869
+ var receiptParam = receiptSchema.optional();
100870
+ var tooBig = (size) => new Error(`Receipt is ${size} bytes, over the ${LIMIT_LABEL} limit (${MAX_RECEIPT_BYTES} bytes)`);
100871
+ function sniff(bytes) {
100872
+ return RECEIPT_TYPES.find((t2) => t2.magic.every((b2, i2) => bytes[i2] === b2));
100873
+ }
100874
+ function decodeBase64(input2) {
100875
+ const stripped = input2.replace(/^data:[^;,]*;base64,/i, "").replace(/\s+/g, "");
100876
+ const decodedSize = Math.floor(stripped.length * 3 / 4) - (stripped.match(/=*$/)?.[0].length ?? 0);
100877
+ if (decodedSize > MAX_RECEIPT_BYTES) throw tooBig(decodedSize);
100878
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(stripped) || stripped.length % 4 === 1) {
100879
+ throw new Error("receipt.base64 is not valid base64");
100880
+ }
100881
+ return Uint8Array.from(Buffer.from(stripped, "base64"));
100882
+ }
100883
+ function cleanFilename(name) {
100884
+ const base = name.split(/[\\/]/).pop() ?? "";
100885
+ return base.replace(/[\u0000-\u001f\u007f"]/g, "").trim();
100886
+ }
100887
+ async function readSandboxed(requested) {
100888
+ const dir = readEnvVar("SPLITWISE_INPUT_DIR");
100889
+ if (!dir) {
100890
+ throw new Error(
100891
+ "receipt.path is disabled: SPLITWISE_INPUT_DIR is not set on this server (and a hosted server cannot see your files). Pass the file as receipt.base64 instead."
100892
+ );
100893
+ }
100894
+ const { open: open2, realpath } = await import("fs/promises");
100895
+ const { constants } = await import("fs");
100896
+ const { isAbsolute: isAbsolute4, relative: relative2, resolve: resolve4, sep: sep2 } = await import("path");
100897
+ let root;
100898
+ try {
100899
+ root = await realpath(dir);
100900
+ } catch (err) {
100901
+ throw new Error(`SPLITWISE_INPUT_DIR (${dir}) is not readable: ${err.message}`);
100902
+ }
100903
+ let target;
100904
+ try {
100905
+ target = await realpath(resolve4(root, requested));
100906
+ } catch (err) {
100907
+ throw new Error(`Cannot read receipt file ${requested}: ${err.code ?? err.message}`);
100908
+ }
100909
+ const rel = relative2(root, target);
100910
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute4(rel)) {
100911
+ throw new Error(`receipt.path ${requested} resolves outside SPLITWISE_INPUT_DIR`);
100912
+ }
100913
+ const handle = await open2(target, constants.O_RDONLY | constants.O_NOFOLLOW);
100914
+ try {
100915
+ const info2 = await handle.stat();
100916
+ if (!info2.isFile()) throw new Error(`Cannot read receipt file ${requested}: not a regular file`);
100917
+ if (info2.size > MAX_RECEIPT_BYTES) throw tooBig(info2.size);
100918
+ return Uint8Array.from(await handle.readFile());
100919
+ } finally {
100920
+ await handle.close();
100921
+ }
100922
+ }
100923
+ async function loadReceipt(input2) {
100924
+ const hasPath = input2.path !== void 0;
100925
+ const hasBase64 = input2.base64 !== void 0;
100926
+ if (hasPath === hasBase64) {
100927
+ throw new Error("receipt needs exactly one of path or base64");
100928
+ }
100929
+ const bytes = hasPath ? await readSandboxed(input2.path) : decodeBase64(input2.base64);
100930
+ if (bytes.length === 0) throw new Error("Receipt file is empty");
100931
+ if (bytes.length > MAX_RECEIPT_BYTES) throw tooBig(bytes.length);
100932
+ const type = sniff(bytes);
100933
+ if (!type) {
100934
+ throw new Error("Receipt must be a PNG, JPEG, GIF or PDF (detected from the file contents)");
100935
+ }
100936
+ if (input2.mime_type !== void 0) {
100937
+ const claimed = input2.mime_type.trim().toLowerCase();
100938
+ if ((MIME_ALIASES[claimed] ?? claimed) !== type.mime) {
100939
+ throw new Error(`receipt.mime_type ${claimed} does not match the file contents (${type.mime})`);
100940
+ }
100941
+ }
100942
+ const filename = cleanFilename(input2.filename) || `receipt.${type.ext}`;
100943
+ const sha256 = createHash4("sha256").update(bytes).digest("hex");
100944
+ return {
100945
+ bytes,
100946
+ filename,
100947
+ mimeType: type.mime,
100948
+ summary: { filename, mime_type: type.mime, bytes: bytes.length, sha256 }
100949
+ };
100950
+ }
100951
+ function buildReceiptForm(fields, receipt) {
100952
+ const form = new FormData();
100953
+ for (const [key, value] of Object.entries(fields)) {
100954
+ if (value === void 0 || value === null) continue;
100955
+ form.append(key, String(value));
100956
+ }
100957
+ form.append("receipt", new Blob([receipt.bytes], { type: receipt.mimeType }), receipt.filename);
100958
+ return form;
100959
+ }
100960
+ function verifyReceiptStored(data, expenseId) {
100961
+ const expense = data?.expenses?.[0];
100962
+ const id = expense?.id ?? expenseId;
100963
+ if (!expense?.receipt?.original) {
100964
+ throw new Error(
100965
+ `Splitwise saved expense ${id ?? "(unknown id)"} but did not attach the receipt (receipt.original is empty). The expense itself was written \u2014 do not re-create it; retry the file with sw_attach_receipt on expense ${id ?? "<id>"}.`
100966
+ );
100967
+ }
100968
+ return { receipt_attached: true, receipt: expense.receipt, ...data };
100969
+ }
100970
+
100620
100971
  // src/tools/expenses.ts
100621
100972
  function flattenUsers(users) {
100622
100973
  const flat = {};
@@ -100633,6 +100984,7 @@ function buildExpenseBody(args) {
100633
100984
  users,
100634
100985
  expense_id: _id,
100635
100986
  confirmToken: _confirmToken,
100987
+ receipt: _receipt,
100636
100988
  ...rest
100637
100989
  } = args;
100638
100990
  if (split_equally && users) {
@@ -100646,6 +100998,14 @@ function buildExpenseBody(args) {
100646
100998
  }
100647
100999
  return body;
100648
101000
  }
101001
+ async function sendExpenseWrite(client2, path, body, receipt, expenseId) {
101002
+ if (!receipt) return client2.request("POST", path, body);
101003
+ const data = await client2.requestMultipart("POST", path, buildReceiptForm(body, receipt));
101004
+ return verifyReceiptStored(data, expenseId);
101005
+ }
101006
+ function previewBody(body, receipt) {
101007
+ return receipt ? { ...body, receipt: receipt.summary } : body;
101008
+ }
100649
101009
  var userShareSchema = external_exports.object({
100650
101010
  user_id: external_exports.number(),
100651
101011
  paid_share: external_exports.string().describe('Amount this user paid, e.g. "25.00"'),
@@ -100708,7 +101068,7 @@ function registerExpenseTools(server, client2) {
100708
101068
  server.registerTool(
100709
101069
  "sw_create_expense",
100710
101070
  {
100711
- 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. This puts charges on every participant's balance and notifies them. ${CONFIRM_NOTE}`,
101071
+ 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. Pass receipt to attach an image or PDF in the same call. This puts charges on every participant's balance and notifies them. ${CONFIRM_NOTE}`,
100712
101072
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
100713
101073
  inputSchema: external_exports.object({
100714
101074
  group_id: external_exports.number().describe("Group to add expense to (use 0 for no group)"),
@@ -100722,31 +101082,33 @@ function registerExpenseTools(server, client2) {
100722
101082
  date: external_exports.string().describe("ISO 8601 datetime").optional(),
100723
101083
  category_id: external_exports.number().describe("Category id from sw_get_categories").optional(),
100724
101084
  details: external_exports.string().describe("Notes").optional(),
101085
+ receipt: receiptParam,
100725
101086
  confirmToken: confirmTokenParam
100726
101087
  })
100727
101088
  },
100728
101089
  async (args, ctx) => {
100729
101090
  const body = buildExpenseBody(args);
101091
+ const receipt = args.receipt ? await loadReceipt(args.receipt) : void 0;
100730
101092
  const gate = await confirmWrite(ctx, {
100731
101093
  tool: "sw_create_expense",
100732
101094
  action: "expense.create",
100733
- summary: `Create a Splitwise expense "${args.description}" (${args.cost}) \u2014 notifies group members`,
101095
+ summary: `Create a Splitwise expense "${args.description}" (${args.cost})${receipt ? ` with receipt ${receipt.filename}` : ""} \u2014 notifies group members`,
100734
101096
  method: "POST",
100735
101097
  path: "/create_expense",
100736
- body,
101098
+ body: previewBody(body, receipt),
100737
101099
  target: args.group_id,
100738
101100
  confirmToken: args.confirmToken,
100739
101101
  args
100740
101102
  });
100741
101103
  if (gate) return gate;
100742
- const data = await client2.request("POST", "/create_expense", body);
101104
+ const data = await sendExpenseWrite(client2, "/create_expense", body, receipt);
100743
101105
  return minifiedResult(data);
100744
101106
  }
100745
101107
  );
100746
101108
  server.registerTool(
100747
101109
  "sw_update_expense",
100748
101110
  {
100749
- 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). ${CONFIRM_NOTE}`,
101111
+ 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). To attach or replace the receipt on an existing expense, pass expense_id and receipt alone. ${CONFIRM_NOTE}`,
100750
101112
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
100751
101113
  inputSchema: external_exports.object({
100752
101114
  expense_id: external_exports.number().describe("ID of the expense to update"),
@@ -100760,25 +101122,58 @@ function registerExpenseTools(server, client2) {
100760
101122
  date: external_exports.string().optional(),
100761
101123
  category_id: external_exports.number().optional(),
100762
101124
  details: external_exports.string().optional(),
101125
+ receipt: receiptParam,
100763
101126
  confirmToken: confirmTokenParam
100764
101127
  })
100765
101128
  },
100766
101129
  async (args, ctx) => {
100767
101130
  const { expense_id } = args;
100768
101131
  const body = buildExpenseBody(args);
101132
+ const receipt = args.receipt ? await loadReceipt(args.receipt) : void 0;
100769
101133
  const gate = await confirmWrite(ctx, {
100770
101134
  tool: "sw_update_expense",
100771
101135
  action: "expense.update",
100772
- summary: `Update Splitwise expense ${expense_id} \u2014 notifies group members`,
101136
+ summary: `Update Splitwise expense ${expense_id}${receipt ? ` (attach receipt ${receipt.filename})` : ""} \u2014 notifies group members`,
100773
101137
  method: "POST",
100774
101138
  path: `/update_expense/${expense_id}`,
100775
- body,
101139
+ body: previewBody(body, receipt),
101140
+ target: expense_id,
101141
+ confirmToken: args.confirmToken,
101142
+ args
101143
+ });
101144
+ if (gate) return gate;
101145
+ const data = await sendExpenseWrite(client2, `/update_expense/${expense_id}`, body, receipt, expense_id);
101146
+ return minifiedResult(data);
101147
+ }
101148
+ );
101149
+ server.registerTool(
101150
+ "sw_attach_receipt",
101151
+ {
101152
+ description: `Attach (or replace) the receipt image or PDF on an existing Splitwise expense. Changes nothing else on the expense \u2014 cost, split and description are not sent. Pass the file as receipt.base64. Fails if Splitwise does not store the file. ${CONFIRM_NOTE}`,
101153
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
101154
+ inputSchema: external_exports.object({
101155
+ expense_id: external_exports.number().describe("ID of the expense to attach the receipt to"),
101156
+ receipt: receiptSchema,
101157
+ confirmToken: confirmTokenParam
101158
+ })
101159
+ },
101160
+ async (args, ctx) => {
101161
+ const { expense_id } = args;
101162
+ const receipt = await loadReceipt(args.receipt);
101163
+ const path = `/update_expense/${expense_id}`;
101164
+ const gate = await confirmWrite(ctx, {
101165
+ tool: "sw_attach_receipt",
101166
+ action: "expense.attach_receipt",
101167
+ summary: `Attach receipt ${receipt.filename} to Splitwise expense ${expense_id} (replaces any existing receipt)`,
101168
+ method: "POST",
101169
+ path,
101170
+ body: previewBody({}, receipt),
100776
101171
  target: expense_id,
100777
101172
  confirmToken: args.confirmToken,
100778
101173
  args
100779
101174
  });
100780
101175
  if (gate) return gate;
100781
- const data = await client2.request("POST", `/update_expense/${expense_id}`, body);
101176
+ const data = await sendExpenseWrite(client2, path, {}, receipt, expense_id);
100782
101177
  return minifiedResult(data);
100783
101178
  }
100784
101179
  );
@@ -100845,7 +101240,7 @@ var MAX_INLINE_BYTES = 4 * 1024 * 1024;
100845
101240
  var MAX_TEXT_CHARS = 1e5;
100846
101241
  var PDF_MIME = "application/pdf";
100847
101242
  var OUTPUT_DIR_NAME = "splitwise-mcp";
100848
- var MAX_RECEIPT_BYTES = 25 * 1024 * 1024;
101243
+ var MAX_RECEIPT_BYTES2 = 25 * 1024 * 1024;
100849
101244
  var RECEIPT_FILE_MODE = 384;
100850
101245
  var GENERIC_TYPES = /* @__PURE__ */ new Set(["application/octet-stream", "binary/octet-stream"]);
100851
101246
  function isPdf(bytes) {
@@ -100857,16 +101252,6 @@ function resolveMimeType(contentType, bytes) {
100857
101252
  const declared = (contentType ?? "").split(";")[0].trim().toLowerCase();
100858
101253
  return declared && !GENERIC_TYPES.has(declared) ? declared : void 0;
100859
101254
  }
100860
- function extensionFromUrl(url4) {
100861
- const match = /\.([a-z0-9]{1,5})$/i.exec(new URL(url4).pathname);
100862
- return match ? match[1].toLowerCase() : void 0;
100863
- }
100864
- function extensionFor(mimeType, url4) {
100865
- const subtype = mimeType?.split("/")[1];
100866
- if (subtype === "jpeg") return "jpg";
100867
- if (subtype && /^[a-z0-9]{1,5}$/.test(subtype)) return subtype;
100868
- return extensionFromUrl(url4) ?? "bin";
100869
- }
100870
101255
  async function extractPdfText(bytes) {
100871
101256
  const { extractText: extractText2, getDocumentProxy: getDocumentProxy2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
100872
101257
  const doc = await getDocumentProxy2(Uint8Array.from(bytes));
@@ -100918,7 +101303,7 @@ function registerReceiptTools(server, client2) {
100918
101303
  throw new Error(`Splitwise expense ${id} has no receipt attached.`);
100919
101304
  }
100920
101305
  const served = receipt?.[wanted] ? wanted : fallback;
100921
- const asset = await client2.fetchAsset(url4, { maxBytes: MAX_RECEIPT_BYTES });
101306
+ const asset = await client2.fetchAsset(url4, { maxBytes: MAX_RECEIPT_BYTES2 });
100922
101307
  if (asset.bytes.length === 0) {
100923
101308
  throw new Error(`Splitwise returned an empty receipt body for expense ${id}.`);
100924
101309
  }
@@ -100938,7 +101323,8 @@ function registerReceiptTools(server, client2) {
100938
101323
  }),
100939
101324
  baseName: `splitwise-receipt-${id}`,
100940
101325
  base64: base643,
100941
- extension: extensionFor(mimeType, url4),
101326
+ // The MIME type, else the receipt URL's own extension, else `bin`.
101327
+ extension: extensionForMime(mimeType, { url: url4 }),
100942
101328
  mode: RECEIPT_FILE_MODE
100943
101329
  });
100944
101330
  } catch (err) {
@@ -101144,7 +101530,7 @@ function registerUtilityTools(server, client2) {
101144
101530
  // src/index.ts
101145
101531
  await runMcp({
101146
101532
  name: "splitwise-mcp",
101147
- version: "3.3.1",
101533
+ version: "3.4.1",
101148
101534
  // x-release-please-version
101149
101535
  deps: client,
101150
101536
  banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",