splitwise-mcp 3.4.0 → 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.
@@ -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.4.0"
10
+ "version": "3.4.1"
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.4.0",
18
+ "version": "3.4.1",
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.4.0",
4
+ "version": "3.4.1",
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/.mcp.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "splitwise": {
4
- "command": "node",
4
+ "command": "npx",
5
5
  "args": [
6
- "${CLAUDE_PLUGIN_ROOT}/dist/index.js"
6
+ "-y",
7
+ "splitwise-mcp"
7
8
  ],
8
9
  "env": {
9
10
  "SPLITWISE_API_KEY": "${SPLITWISE_API_KEY}",
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++) {
@@ -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();`);
@@ -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++) {
@@ -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";
@@ -101036,16 +101252,6 @@ function resolveMimeType(contentType, bytes) {
101036
101252
  const declared = (contentType ?? "").split(";")[0].trim().toLowerCase();
101037
101253
  return declared && !GENERIC_TYPES.has(declared) ? declared : void 0;
101038
101254
  }
101039
- function extensionFromUrl(url4) {
101040
- const match = /\.([a-z0-9]{1,5})$/i.exec(new URL(url4).pathname);
101041
- return match ? match[1].toLowerCase() : void 0;
101042
- }
101043
- function extensionFor(mimeType, url4) {
101044
- const subtype = mimeType?.split("/")[1];
101045
- if (subtype === "jpeg") return "jpg";
101046
- if (subtype && /^[a-z0-9]{1,5}$/.test(subtype)) return subtype;
101047
- return extensionFromUrl(url4) ?? "bin";
101048
- }
101049
101255
  async function extractPdfText(bytes) {
101050
101256
  const { extractText: extractText2, getDocumentProxy: getDocumentProxy2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
101051
101257
  const doc = await getDocumentProxy2(Uint8Array.from(bytes));
@@ -101117,7 +101323,8 @@ function registerReceiptTools(server, client2) {
101117
101323
  }),
101118
101324
  baseName: `splitwise-receipt-${id}`,
101119
101325
  base64: base643,
101120
- extension: extensionFor(mimeType, url4),
101326
+ // The MIME type, else the receipt URL's own extension, else `bin`.
101327
+ extension: extensionForMime(mimeType, { url: url4 }),
101121
101328
  mode: RECEIPT_FILE_MODE
101122
101329
  });
101123
101330
  } catch (err) {
@@ -101323,7 +101530,7 @@ function registerUtilityTools(server, client2) {
101323
101530
  // src/index.ts
101324
101531
  await runMcp({
101325
101532
  name: "splitwise-mcp",
101326
- version: "3.4.0",
101533
+ version: "3.4.1",
101327
101534
  // x-release-please-version
101328
101535
  deps: client,
101329
101536
  banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
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.4.0', // x-release-please-version
22
+ version: '3.4.1', // 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: [
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { minifiedResult, imageResult, readEnvVar, resolveOutputDir, writeBinaryOutput, sniffMimeBytes, } from '@chrischall/mcp-utils';
2
+ import { minifiedResult, imageResult, readEnvVar, resolveOutputDir, writeBinaryOutput, sniffMimeBytes, extensionForMime, } from '@chrischall/mcp-utils';
3
3
  import { UNTRUSTED_DESCRIPTION_SUFFIX, swUntrustedEnvelope } from './_untrusted.js';
4
4
  /** Cap on bytes returned as base64 in the tool result. */
5
5
  const MAX_INLINE_BYTES = 4 * 1024 * 1024;
@@ -49,24 +49,6 @@ function resolveMimeType(contentType, bytes) {
49
49
  const declared = (contentType ?? '').split(';')[0].trim().toLowerCase();
50
50
  return declared && !GENERIC_TYPES.has(declared) ? declared : undefined;
51
51
  }
52
- /** Extension from a URL path (`…/receipt.jpg?sig=…` → `jpg`), when it has one. */
53
- function extensionFromUrl(url) {
54
- const match = /\.([a-z0-9]{1,5})$/i.exec(new URL(url).pathname);
55
- return match ? match[1].toLowerCase() : undefined;
56
- }
57
- /**
58
- * File extension for the receipt: the MIME subtype when it looks like a plain
59
- * one (`application/pdf` → `pdf`, `image/heic` → `heic`), else the URL's own
60
- * extension, else `bin`.
61
- */
62
- function extensionFor(mimeType, url) {
63
- const subtype = mimeType?.split('/')[1];
64
- if (subtype === 'jpeg')
65
- return 'jpg';
66
- if (subtype && /^[a-z0-9]{1,5}$/.test(subtype))
67
- return subtype;
68
- return extensionFromUrl(url) ?? 'bin';
69
- }
70
52
  /**
71
53
  * Extract a PDF's text layer. `unpdf` is imported lazily so the cost lands only
72
54
  * on calls that ask for text — it is a sizable pdf.js build, and every other
@@ -162,7 +144,8 @@ export function registerReceiptTools(server, client) {
162
144
  }),
163
145
  baseName: `splitwise-receipt-${id}`,
164
146
  base64,
165
- extension: extensionFor(mimeType, url),
147
+ // The MIME type, else the receipt URL's own extension, else `bin`.
148
+ extension: extensionForMime(mimeType, { url }),
166
149
  mode: RECEIPT_FILE_MODE,
167
150
  });
168
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
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,7 +45,7 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^3.0.0",
48
+ "@chrischall/mcp-utils": "^3.0.3",
49
49
  "@modelcontextprotocol/server": "^2.3.0",
50
50
  "dotenv": "^18.0.2",
51
51
  "unpdf": "^1.8.1",
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.4.0",
9
+ "version": "3.4.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "splitwise-mcp",
14
- "version": "3.4.0",
14
+ "version": "3.4.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },