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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +5 -3
- package/README.md +7 -2
- package/dist/bundle.js +444 -58
- package/dist/client.js +14 -2
- package/dist/index.js +1 -1
- package/dist/tools/_receipt.js +173 -0
- package/dist/tools/expenses.js +60 -10
- package/dist/tools/receipts.js +3 -20
- package/package.json +2 -2
- package/server.json +9 -2
- package/skills/splitwise-mcp/SKILL.md +15 -4
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
|
|
1964
|
+
const tooBig2 = size > def.size;
|
|
1952
1965
|
payload.issues.push({
|
|
1953
1966
|
origin: util.getSizableOrigin(input2),
|
|
1954
|
-
...
|
|
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
|
|
2031
|
+
const tooBig2 = length > def.length;
|
|
2019
2032
|
payload.issues.push({
|
|
2020
2033
|
origin,
|
|
2021
|
-
...
|
|
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:
|
|
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(`([
|
|
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
|
|
80591
|
+
const tooBig2 = size > def.size;
|
|
80528
80592
|
payload.issues.push({
|
|
80529
80593
|
origin: getSizableOrigin(input2),
|
|
80530
|
-
...
|
|
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
|
|
80658
|
+
const tooBig2 = length > def.length;
|
|
80595
80659
|
payload.issues.push({
|
|
80596
80660
|
origin,
|
|
80597
|
-
...
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
99521
|
-
let next;
|
|
99721
|
+
function resolveLocation(location, from) {
|
|
99522
99722
|
try {
|
|
99523
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.",
|