splitwise-mcp 2.0.7 → 2.0.9
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/LICENSE +21 -0
- package/README.md +4 -0
- package/dist/bundle.js +537 -187
- package/dist/client.js +30 -55
- package/dist/index.js +18 -13
- package/dist/tools/expenses.js +20 -16
- package/dist/tools/friends.js +6 -4
- package/dist/tools/groups.js +10 -8
- package/dist/tools/user.js +6 -4
- package/dist/tools/utilities.js +12 -8
- package/package.json +2 -1
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -3105,6 +3105,9 @@ var require_utils = __commonJS({
|
|
|
3105
3105
|
"use strict";
|
|
3106
3106
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3107
3107
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3108
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3109
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3110
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3108
3111
|
function stringArrayToHexStripped(input) {
|
|
3109
3112
|
let acc = "";
|
|
3110
3113
|
let code = 0;
|
|
@@ -3297,27 +3300,77 @@ var require_utils = __commonJS({
|
|
|
3297
3300
|
}
|
|
3298
3301
|
return output.join("");
|
|
3299
3302
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3303
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3304
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3305
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3306
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3307
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3308
|
+
re.lastIndex = 0;
|
|
3309
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3310
|
+
}
|
|
3311
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3312
|
+
if (input.indexOf("%") === -1) {
|
|
3313
|
+
return input;
|
|
3310
3314
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3315
|
+
let output = "";
|
|
3316
|
+
for (let i = 0; i < input.length; i++) {
|
|
3317
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3318
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3319
|
+
if (isHexPair(hex3)) {
|
|
3320
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3321
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3322
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3323
|
+
output += decoded;
|
|
3324
|
+
} else {
|
|
3325
|
+
output += "%" + normalizedHex;
|
|
3326
|
+
}
|
|
3327
|
+
i += 2;
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
output += input[i];
|
|
3313
3332
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3333
|
+
return output;
|
|
3334
|
+
}
|
|
3335
|
+
function normalizePathEncoding(input) {
|
|
3336
|
+
let output = "";
|
|
3337
|
+
for (let i = 0; i < input.length; i++) {
|
|
3338
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3339
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3340
|
+
if (isHexPair(hex3)) {
|
|
3341
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3342
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3343
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3344
|
+
output += decoded;
|
|
3345
|
+
} else {
|
|
3346
|
+
output += "%" + normalizedHex;
|
|
3347
|
+
}
|
|
3348
|
+
i += 2;
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (isPathCharacter(input[i])) {
|
|
3353
|
+
output += input[i];
|
|
3354
|
+
} else {
|
|
3355
|
+
output += escape(input[i]);
|
|
3356
|
+
}
|
|
3316
3357
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3358
|
+
return output;
|
|
3359
|
+
}
|
|
3360
|
+
function escapePreservingEscapes(input) {
|
|
3361
|
+
let output = "";
|
|
3362
|
+
for (let i = 0; i < input.length; i++) {
|
|
3363
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3364
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3365
|
+
if (isHexPair(hex3)) {
|
|
3366
|
+
output += "%" + hex3.toUpperCase();
|
|
3367
|
+
i += 2;
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
output += escape(input[i]);
|
|
3319
3372
|
}
|
|
3320
|
-
return
|
|
3373
|
+
return output;
|
|
3321
3374
|
}
|
|
3322
3375
|
function recomposeAuthority(component) {
|
|
3323
3376
|
const uriTokens = [];
|
|
@@ -3332,7 +3385,7 @@ var require_utils = __commonJS({
|
|
|
3332
3385
|
if (ipV6res.isIPV6 === true) {
|
|
3333
3386
|
host = `[${ipV6res.escapedHost}]`;
|
|
3334
3387
|
} else {
|
|
3335
|
-
host =
|
|
3388
|
+
host = reescapeHostDelimiters(host, false);
|
|
3336
3389
|
}
|
|
3337
3390
|
}
|
|
3338
3391
|
uriTokens.push(host);
|
|
@@ -3346,7 +3399,10 @@ var require_utils = __commonJS({
|
|
|
3346
3399
|
module.exports = {
|
|
3347
3400
|
nonSimpleDomain,
|
|
3348
3401
|
recomposeAuthority,
|
|
3349
|
-
|
|
3402
|
+
reescapeHostDelimiters,
|
|
3403
|
+
normalizePercentEncoding,
|
|
3404
|
+
normalizePathEncoding,
|
|
3405
|
+
escapePreservingEscapes,
|
|
3350
3406
|
removeDotSegments,
|
|
3351
3407
|
isIPv4,
|
|
3352
3408
|
isUUID,
|
|
@@ -3570,12 +3626,12 @@ var require_schemes = __commonJS({
|
|
|
3570
3626
|
var require_fast_uri = __commonJS({
|
|
3571
3627
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
3572
3628
|
"use strict";
|
|
3573
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3629
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3574
3630
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3575
3631
|
function normalize(uri, options) {
|
|
3576
3632
|
if (typeof uri === "string") {
|
|
3577
3633
|
uri = /** @type {T} */
|
|
3578
|
-
|
|
3634
|
+
normalizeString(uri, options);
|
|
3579
3635
|
} else if (typeof uri === "object") {
|
|
3580
3636
|
uri = /** @type {T} */
|
|
3581
3637
|
parse3(serialize(uri, options), options);
|
|
@@ -3642,19 +3698,9 @@ var require_fast_uri = __commonJS({
|
|
|
3642
3698
|
return target;
|
|
3643
3699
|
}
|
|
3644
3700
|
function equal(uriA, uriB, options) {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
} else if (typeof uriA === "object") {
|
|
3649
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3650
|
-
}
|
|
3651
|
-
if (typeof uriB === "string") {
|
|
3652
|
-
uriB = unescape(uriB);
|
|
3653
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3654
|
-
} else if (typeof uriB === "object") {
|
|
3655
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3656
|
-
}
|
|
3657
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3701
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3702
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3703
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3658
3704
|
}
|
|
3659
3705
|
function serialize(cmpts, opts) {
|
|
3660
3706
|
const component = {
|
|
@@ -3679,12 +3725,12 @@ var require_fast_uri = __commonJS({
|
|
|
3679
3725
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3680
3726
|
if (component.path !== void 0) {
|
|
3681
3727
|
if (!options.skipEscape) {
|
|
3682
|
-
component.path =
|
|
3728
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3683
3729
|
if (component.scheme !== void 0) {
|
|
3684
3730
|
component.path = component.path.split("%3A").join(":");
|
|
3685
3731
|
}
|
|
3686
3732
|
} else {
|
|
3687
|
-
component.path =
|
|
3733
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3688
3734
|
}
|
|
3689
3735
|
}
|
|
3690
3736
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3719,7 +3765,16 @@ var require_fast_uri = __commonJS({
|
|
|
3719
3765
|
return uriTokens.join("");
|
|
3720
3766
|
}
|
|
3721
3767
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3722
|
-
function
|
|
3768
|
+
function getParseError(parsed, matches) {
|
|
3769
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3770
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3771
|
+
}
|
|
3772
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3773
|
+
return "URI port is malformed.";
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
function parseWithStatus(uri, opts) {
|
|
3723
3778
|
const options = Object.assign({}, opts);
|
|
3724
3779
|
const parsed = {
|
|
3725
3780
|
scheme: void 0,
|
|
@@ -3730,6 +3785,7 @@ var require_fast_uri = __commonJS({
|
|
|
3730
3785
|
query: void 0,
|
|
3731
3786
|
fragment: void 0
|
|
3732
3787
|
};
|
|
3788
|
+
let malformedAuthorityOrPort = false;
|
|
3733
3789
|
let isIP = false;
|
|
3734
3790
|
if (options.reference === "suffix") {
|
|
3735
3791
|
if (options.scheme) {
|
|
@@ -3750,6 +3806,11 @@ var require_fast_uri = __commonJS({
|
|
|
3750
3806
|
if (isNaN(parsed.port)) {
|
|
3751
3807
|
parsed.port = matches[5];
|
|
3752
3808
|
}
|
|
3809
|
+
const parseError = getParseError(parsed, matches);
|
|
3810
|
+
if (parseError !== void 0) {
|
|
3811
|
+
parsed.error = parsed.error || parseError;
|
|
3812
|
+
malformedAuthorityOrPort = true;
|
|
3813
|
+
}
|
|
3753
3814
|
if (parsed.host) {
|
|
3754
3815
|
const ipv4result = isIPv4(parsed.host);
|
|
3755
3816
|
if (ipv4result === false) {
|
|
@@ -3788,14 +3849,18 @@ var require_fast_uri = __commonJS({
|
|
|
3788
3849
|
parsed.scheme = unescape(parsed.scheme);
|
|
3789
3850
|
}
|
|
3790
3851
|
if (parsed.host !== void 0) {
|
|
3791
|
-
parsed.host = unescape(parsed.host);
|
|
3852
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3792
3853
|
}
|
|
3793
3854
|
}
|
|
3794
3855
|
if (parsed.path) {
|
|
3795
|
-
parsed.path =
|
|
3856
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3796
3857
|
}
|
|
3797
3858
|
if (parsed.fragment) {
|
|
3798
|
-
|
|
3859
|
+
try {
|
|
3860
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3861
|
+
} catch {
|
|
3862
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3863
|
+
}
|
|
3799
3864
|
}
|
|
3800
3865
|
}
|
|
3801
3866
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3804,7 +3869,29 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3869
|
} else {
|
|
3805
3870
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3806
3871
|
}
|
|
3807
|
-
return parsed;
|
|
3872
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3873
|
+
}
|
|
3874
|
+
function parse3(uri, opts) {
|
|
3875
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3876
|
+
}
|
|
3877
|
+
function normalizeString(uri, opts) {
|
|
3878
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3879
|
+
}
|
|
3880
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3881
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3882
|
+
return {
|
|
3883
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3884
|
+
malformedAuthorityOrPort
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
function normalizeComparableURI(uri, opts) {
|
|
3888
|
+
if (typeof uri === "string") {
|
|
3889
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3890
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3891
|
+
}
|
|
3892
|
+
if (typeof uri === "object") {
|
|
3893
|
+
return serialize(uri, opts);
|
|
3894
|
+
}
|
|
3808
3895
|
}
|
|
3809
3896
|
var fastUri = {
|
|
3810
3897
|
SCHEMES,
|
|
@@ -28416,11 +28503,11 @@ var Protocol = class {
|
|
|
28416
28503
|
*
|
|
28417
28504
|
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
28418
28505
|
*/
|
|
28419
|
-
async connect(
|
|
28506
|
+
async connect(transport) {
|
|
28420
28507
|
if (this._transport) {
|
|
28421
28508
|
throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
|
|
28422
28509
|
}
|
|
28423
|
-
this._transport =
|
|
28510
|
+
this._transport = transport;
|
|
28424
28511
|
const _onclose = this.transport?.onclose;
|
|
28425
28512
|
this._transport.onclose = () => {
|
|
28426
28513
|
_onclose?.();
|
|
@@ -30010,8 +30097,8 @@ var McpServer = class {
|
|
|
30010
30097
|
*
|
|
30011
30098
|
* The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
30012
30099
|
*/
|
|
30013
|
-
async connect(
|
|
30014
|
-
return await this.server.connect(
|
|
30100
|
+
async connect(transport) {
|
|
30101
|
+
return await this.server.connect(transport);
|
|
30015
30102
|
}
|
|
30016
30103
|
/**
|
|
30017
30104
|
* Closes the connection.
|
|
@@ -30861,35 +30948,310 @@ var StdioServerTransport = class {
|
|
|
30861
30948
|
}
|
|
30862
30949
|
};
|
|
30863
30950
|
|
|
30951
|
+
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
30952
|
+
async function createMcpServer(opts) {
|
|
30953
|
+
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
30954
|
+
if (opts.banner !== void 0) {
|
|
30955
|
+
console.error(opts.banner);
|
|
30956
|
+
}
|
|
30957
|
+
const deps = opts.deps;
|
|
30958
|
+
for (const register of opts.tools) {
|
|
30959
|
+
await register(server, deps);
|
|
30960
|
+
}
|
|
30961
|
+
return server;
|
|
30962
|
+
}
|
|
30963
|
+
function withGracefulShutdown(server, opts = {}) {
|
|
30964
|
+
const shouldExit = opts.exit ?? true;
|
|
30965
|
+
let shuttingDown = false;
|
|
30966
|
+
const handler = (signal) => {
|
|
30967
|
+
if (shuttingDown)
|
|
30968
|
+
return;
|
|
30969
|
+
shuttingDown = true;
|
|
30970
|
+
void (async () => {
|
|
30971
|
+
try {
|
|
30972
|
+
if (opts.onSignal)
|
|
30973
|
+
await opts.onSignal(signal);
|
|
30974
|
+
await server.close();
|
|
30975
|
+
} catch (err) {
|
|
30976
|
+
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
30977
|
+
} finally {
|
|
30978
|
+
if (shouldExit)
|
|
30979
|
+
process.exit(0);
|
|
30980
|
+
}
|
|
30981
|
+
})();
|
|
30982
|
+
};
|
|
30983
|
+
process.on("SIGINT", () => handler("SIGINT"));
|
|
30984
|
+
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
30985
|
+
}
|
|
30986
|
+
async function runMcp(opts) {
|
|
30987
|
+
const server = await createMcpServer(opts);
|
|
30988
|
+
const shutdown = opts.shutdown ?? true;
|
|
30989
|
+
if (shutdown !== false) {
|
|
30990
|
+
withGracefulShutdown(server, shutdown === true ? {} : shutdown);
|
|
30991
|
+
}
|
|
30992
|
+
const spec = opts.transport ?? "stdio";
|
|
30993
|
+
const transport = spec === "stdio" ? new StdioServerTransport() : spec;
|
|
30994
|
+
await server.connect(transport);
|
|
30995
|
+
return server;
|
|
30996
|
+
}
|
|
30997
|
+
|
|
30998
|
+
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
30999
|
+
var DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
31000
|
+
var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
31001
|
+
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
31002
|
+
var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
|
|
31003
|
+
var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
|
|
31004
|
+
var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
|
|
31005
|
+
var API_KEY_RE = new RegExp([
|
|
31006
|
+
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
31007
|
+
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
31008
|
+
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
31009
|
+
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
31010
|
+
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
31011
|
+
// Slack
|
|
31012
|
+
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
31013
|
+
// Google API key (39 chars total)
|
|
31014
|
+
"AKIA[0-9A-Z]{16}\\b",
|
|
31015
|
+
// AWS access key id (20 chars total)
|
|
31016
|
+
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
31017
|
+
// webhook signing secret (Stripe-style)
|
|
31018
|
+
].map((p) => `\\b${p}`).join("|"), "g");
|
|
31019
|
+
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
31020
|
+
function redactSecrets(text) {
|
|
31021
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
31022
|
+
}
|
|
31023
|
+
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
31024
|
+
const str = text === null || text === void 0 ? "" : String(text);
|
|
31025
|
+
const redacted = redactSecrets(str);
|
|
31026
|
+
if (redacted.length <= max)
|
|
31027
|
+
return redacted;
|
|
31028
|
+
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
31029
|
+
}
|
|
31030
|
+
|
|
31031
|
+
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
31032
|
+
function textResult(data) {
|
|
31033
|
+
return {
|
|
31034
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
31035
|
+
};
|
|
31036
|
+
}
|
|
31037
|
+
|
|
31038
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
31039
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
31040
|
+
function readEnvVar(key, opts = {}) {
|
|
31041
|
+
const env = opts.env ?? process.env;
|
|
31042
|
+
const raw = env[key];
|
|
31043
|
+
if (typeof raw === "string") {
|
|
31044
|
+
const trimmed = raw.trim();
|
|
31045
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
31046
|
+
return trimmed;
|
|
31047
|
+
}
|
|
31048
|
+
}
|
|
31049
|
+
return opts.default;
|
|
31050
|
+
}
|
|
31051
|
+
async function loadDotenvSafely(opts = {}) {
|
|
31052
|
+
try {
|
|
31053
|
+
const mod = await import(
|
|
31054
|
+
/* @vite-ignore */
|
|
31055
|
+
"dotenv"
|
|
31056
|
+
);
|
|
31057
|
+
const result = mod.config({
|
|
31058
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
31059
|
+
override: opts.override ?? false,
|
|
31060
|
+
quiet: true
|
|
31061
|
+
});
|
|
31062
|
+
return result.error === void 0;
|
|
31063
|
+
} catch {
|
|
31064
|
+
return false;
|
|
31065
|
+
}
|
|
31066
|
+
}
|
|
31067
|
+
|
|
31068
|
+
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
31069
|
+
var DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
31070
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31071
|
+
var UnauthorizedError = class extends Error {
|
|
31072
|
+
status = 401;
|
|
31073
|
+
constructor(service) {
|
|
31074
|
+
super(`Unauthorized (401) from ${service} \u2014 the token is missing, invalid, or expired.`);
|
|
31075
|
+
this.name = "UnauthorizedError";
|
|
31076
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31077
|
+
}
|
|
31078
|
+
};
|
|
31079
|
+
var RateLimitedError = class extends Error {
|
|
31080
|
+
status = 429;
|
|
31081
|
+
constructor(service) {
|
|
31082
|
+
super(`Rate limited (429) by ${service} after retries.`);
|
|
31083
|
+
this.name = "RateLimitedError";
|
|
31084
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31085
|
+
}
|
|
31086
|
+
};
|
|
31087
|
+
var RequestTimeoutError = class extends Error {
|
|
31088
|
+
timeoutMs;
|
|
31089
|
+
constructor(service, timeoutMs) {
|
|
31090
|
+
super(`Request to ${service} timed out after ${timeoutMs}ms.`);
|
|
31091
|
+
this.name = "RequestTimeoutError";
|
|
31092
|
+
this.timeoutMs = timeoutMs;
|
|
31093
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31094
|
+
}
|
|
31095
|
+
};
|
|
31096
|
+
var ApiError = class extends Error {
|
|
31097
|
+
status;
|
|
31098
|
+
constructor(status, message) {
|
|
31099
|
+
super(message);
|
|
31100
|
+
this.name = "ApiError";
|
|
31101
|
+
this.status = status;
|
|
31102
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31103
|
+
}
|
|
31104
|
+
};
|
|
31105
|
+
function hostOf(baseUrl) {
|
|
31106
|
+
try {
|
|
31107
|
+
return new URL(baseUrl).host;
|
|
31108
|
+
} catch {
|
|
31109
|
+
return baseUrl;
|
|
31110
|
+
}
|
|
31111
|
+
}
|
|
31112
|
+
function createApiClient(opts) {
|
|
31113
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
31114
|
+
const retry = opts.retry ?? DEFAULT_RETRY;
|
|
31115
|
+
const service = opts.serviceName ?? hostOf(opts.baseUrl);
|
|
31116
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
31117
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
31118
|
+
const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
|
|
31119
|
+
const rateLimited = () => opts.onRateLimited ? opts.onRateLimited() : new RateLimitedError(service);
|
|
31120
|
+
const retryStatuses = retry.statuses ?? [429];
|
|
31121
|
+
const timeoutMs = opts.timeout;
|
|
31122
|
+
const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
|
|
31123
|
+
function withTimeout(run) {
|
|
31124
|
+
if (timeoutMs == null || timeoutMs <= 0)
|
|
31125
|
+
return run(void 0);
|
|
31126
|
+
const controller = new AbortController();
|
|
31127
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
31128
|
+
return run(controller.signal).catch((err) => {
|
|
31129
|
+
if (err instanceof Error && err.name === "AbortError")
|
|
31130
|
+
throw new RequestTimeoutError(service, timeoutMs);
|
|
31131
|
+
throw err;
|
|
31132
|
+
}).finally(() => clearTimeout(timer));
|
|
31133
|
+
}
|
|
31134
|
+
async function send(method, path, opt) {
|
|
31135
|
+
const isMultipart = opt.formData !== void 0;
|
|
31136
|
+
const hasJsonBody = !isMultipart && opt.body !== void 0;
|
|
31137
|
+
const reqBody = isMultipart ? opt.formData : hasJsonBody ? JSON.stringify(opt.body) : void 0;
|
|
31138
|
+
const query = opt.query ? buildQueryString(opt.query) : "";
|
|
31139
|
+
const url2 = `${base}${path}${query}`;
|
|
31140
|
+
const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
|
|
31141
|
+
const fetchWith = (token, signal) => doFetch(url2, {
|
|
31142
|
+
method,
|
|
31143
|
+
headers: {
|
|
31144
|
+
Accept: "application/json",
|
|
31145
|
+
...hasJsonBody ? { "Content-Type": "application/json" } : {},
|
|
31146
|
+
...opts.baseHeaders,
|
|
31147
|
+
...authHeader(token || void 0),
|
|
31148
|
+
...opt.headers
|
|
31149
|
+
},
|
|
31150
|
+
...signal ? { signal } : {},
|
|
31151
|
+
...bodyInit
|
|
31152
|
+
});
|
|
31153
|
+
const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
|
|
31154
|
+
let attempt = 0;
|
|
31155
|
+
for (; ; ) {
|
|
31156
|
+
const res = await once();
|
|
31157
|
+
if (retryStatuses.includes(res.status) && attempt < retry.count) {
|
|
31158
|
+
attempt += 1;
|
|
31159
|
+
await sleep(retry.delayMs);
|
|
31160
|
+
continue;
|
|
31161
|
+
}
|
|
31162
|
+
return res;
|
|
31163
|
+
}
|
|
31164
|
+
}
|
|
31165
|
+
async function fetchJson(method, path, opt = {}) {
|
|
31166
|
+
const res = await send(method, path, opt);
|
|
31167
|
+
if (res.status === 401)
|
|
31168
|
+
throw unauthorized();
|
|
31169
|
+
if (res.status === 429)
|
|
31170
|
+
throw rateLimited();
|
|
31171
|
+
if (res.status === 204)
|
|
31172
|
+
return void 0;
|
|
31173
|
+
const text = await res.text();
|
|
31174
|
+
if (!res.ok) {
|
|
31175
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
31176
|
+
}
|
|
31177
|
+
if (text.length === 0)
|
|
31178
|
+
return void 0;
|
|
31179
|
+
return JSON.parse(text);
|
|
31180
|
+
}
|
|
31181
|
+
async function fetchHtml(method, path, opt = {}) {
|
|
31182
|
+
const headers = { Accept: "text/html,*/*", ...opt.headers };
|
|
31183
|
+
const res = await send(method, path, { ...opt, headers });
|
|
31184
|
+
if (res.status === 401)
|
|
31185
|
+
throw unauthorized();
|
|
31186
|
+
if (res.status === 429)
|
|
31187
|
+
throw rateLimited();
|
|
31188
|
+
const text = await res.text();
|
|
31189
|
+
if (!res.ok) {
|
|
31190
|
+
throw new ApiError(res.status, formatApiError(res.status, method, path, text, { service }));
|
|
31191
|
+
}
|
|
31192
|
+
return text;
|
|
31193
|
+
}
|
|
31194
|
+
return { fetchJson, fetchHtml };
|
|
31195
|
+
}
|
|
31196
|
+
function buildQueryString(params) {
|
|
31197
|
+
const parts = [];
|
|
31198
|
+
for (const [key, value] of Object.entries(params)) {
|
|
31199
|
+
if (value === void 0 || value === null || value === "")
|
|
31200
|
+
continue;
|
|
31201
|
+
if (Array.isArray(value)) {
|
|
31202
|
+
for (const item of value) {
|
|
31203
|
+
if (item === void 0 || item === null || item === "")
|
|
31204
|
+
continue;
|
|
31205
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
|
|
31206
|
+
}
|
|
31207
|
+
} else {
|
|
31208
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
31209
|
+
}
|
|
31210
|
+
}
|
|
31211
|
+
return parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
31212
|
+
}
|
|
31213
|
+
function formatApiError(status, method, path, errorText, opts = {}) {
|
|
31214
|
+
const service = opts.service ?? "API";
|
|
31215
|
+
const head = `${service} error ${status} for ${method.toUpperCase()} ${path}`;
|
|
31216
|
+
const safe = truncateErrorMessage(errorText ?? "", opts.max).trim();
|
|
31217
|
+
return safe.length > 0 ? `${head}: ${safe}` : head;
|
|
31218
|
+
}
|
|
31219
|
+
|
|
31220
|
+
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
31221
|
+
var PositiveInt = external_exports.number().int().positive();
|
|
31222
|
+
var NonNegInt = external_exports.number().int().nonnegative();
|
|
31223
|
+
var NonEmptyString = external_exports.string().min(1);
|
|
31224
|
+
var IsoDate = external_exports.iso.date();
|
|
31225
|
+
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
31226
|
+
var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
|
|
31227
|
+
var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
|
|
31228
|
+
var paginationSchema = {
|
|
31229
|
+
offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
|
|
31230
|
+
limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
|
|
31231
|
+
};
|
|
31232
|
+
var pageSchema = {
|
|
31233
|
+
page_num: PositiveInt.default(1).describe("1-based page number."),
|
|
31234
|
+
page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
|
|
31235
|
+
};
|
|
31236
|
+
|
|
30864
31237
|
// src/client.ts
|
|
30865
31238
|
import { dirname, join } from "path";
|
|
30866
31239
|
import { fileURLToPath } from "url";
|
|
30867
|
-
|
|
30868
|
-
|
|
30869
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30870
|
-
config2({ path: join(__dirname, "..", ".env"), override: false, quiet: true });
|
|
30871
|
-
} catch {
|
|
30872
|
-
}
|
|
30873
|
-
function readVar(key) {
|
|
30874
|
-
const raw = process.env[key];
|
|
30875
|
-
if (typeof raw !== "string") return void 0;
|
|
30876
|
-
const trimmed = raw.trim();
|
|
30877
|
-
if (trimmed.length === 0) return void 0;
|
|
30878
|
-
if (trimmed === "undefined" || trimmed === "null") return void 0;
|
|
30879
|
-
if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
|
|
30880
|
-
return trimmed;
|
|
30881
|
-
}
|
|
31240
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31241
|
+
await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
|
|
30882
31242
|
var BASE_URL = "https://secure.splitwise.com/api/v3.0";
|
|
31243
|
+
var SERVICE_NAME = "Splitwise";
|
|
30883
31244
|
var SplitwiseClient = class {
|
|
30884
31245
|
apiKey;
|
|
30885
31246
|
configError;
|
|
31247
|
+
api;
|
|
30886
31248
|
/**
|
|
30887
31249
|
* Defer the config error so the server can still start (and respond to the
|
|
30888
31250
|
* host's install-time smoke test) when SPLITWISE_API_KEY isn't set yet.
|
|
30889
31251
|
* Tool calls re-raise the error at request time.
|
|
30890
31252
|
*/
|
|
30891
31253
|
constructor() {
|
|
30892
|
-
const key =
|
|
31254
|
+
const key = readEnvVar("SPLITWISE_API_KEY");
|
|
30893
31255
|
if (!key) {
|
|
30894
31256
|
this.apiKey = null;
|
|
30895
31257
|
this.configError = new Error("SPLITWISE_API_KEY environment variable is required");
|
|
@@ -30897,64 +31259,46 @@ var SplitwiseClient = class {
|
|
|
30897
31259
|
this.apiKey = key;
|
|
30898
31260
|
this.configError = null;
|
|
30899
31261
|
}
|
|
31262
|
+
this.api = createApiClient({
|
|
31263
|
+
baseUrl: BASE_URL,
|
|
31264
|
+
getToken: () => this.requireKey(),
|
|
31265
|
+
serviceName: SERVICE_NAME,
|
|
31266
|
+
retry: { count: 1, delayMs: 2e3 },
|
|
31267
|
+
timeout: 3e4,
|
|
31268
|
+
onUnauthorized: () => new Error("SPLITWISE_API_KEY is invalid or missing"),
|
|
31269
|
+
onRateLimited: () => new Error("Rate limited by Splitwise API")
|
|
31270
|
+
});
|
|
30900
31271
|
}
|
|
30901
31272
|
requireKey() {
|
|
30902
31273
|
if (this.configError) throw this.configError;
|
|
30903
31274
|
return this.apiKey;
|
|
30904
31275
|
}
|
|
30905
31276
|
async request(method, path, body) {
|
|
30906
|
-
return this.
|
|
30907
|
-
}
|
|
30908
|
-
async doRequest(method, path, body, isRetry) {
|
|
30909
|
-
const headers = {
|
|
30910
|
-
Authorization: `Bearer ${this.requireKey()}`,
|
|
30911
|
-
"Content-Type": "application/json",
|
|
30912
|
-
Accept: "application/json"
|
|
30913
|
-
};
|
|
30914
|
-
const response = await fetch(`${BASE_URL}${path}`, {
|
|
30915
|
-
method,
|
|
30916
|
-
headers,
|
|
30917
|
-
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
30918
|
-
});
|
|
30919
|
-
if (response.status === 401) {
|
|
30920
|
-
throw new Error("SPLITWISE_API_KEY is invalid or missing");
|
|
30921
|
-
}
|
|
30922
|
-
if (response.status === 429) {
|
|
30923
|
-
if (!isRetry) {
|
|
30924
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
30925
|
-
return this.doRequest(method, path, body, true);
|
|
30926
|
-
}
|
|
30927
|
-
throw new Error("Rate limited by Splitwise API");
|
|
30928
|
-
}
|
|
30929
|
-
if (!response.ok) {
|
|
30930
|
-
throw new Error(
|
|
30931
|
-
`Splitwise API error: ${response.status} ${response.statusText} for ${method} ${path}`
|
|
30932
|
-
);
|
|
30933
|
-
}
|
|
30934
|
-
return response.json();
|
|
31277
|
+
return this.api.fetchJson(method, path, body !== void 0 ? { body } : {});
|
|
30935
31278
|
}
|
|
30936
31279
|
};
|
|
31280
|
+
var client = new SplitwiseClient();
|
|
30937
31281
|
|
|
30938
31282
|
// src/tools/user.ts
|
|
30939
|
-
function registerUserTools(
|
|
30940
|
-
|
|
31283
|
+
function registerUserTools(server) {
|
|
31284
|
+
server.registerTool("sw_get_current_user", {
|
|
30941
31285
|
description: "Get the authenticated Splitwise user's profile (id, first_name, last_name, email). Use the returned id when building custom expense splits.",
|
|
30942
31286
|
annotations: { readOnlyHint: true }
|
|
30943
31287
|
}, async () => {
|
|
30944
|
-
const data = await
|
|
30945
|
-
return
|
|
31288
|
+
const data = await client.request("GET", "/get_current_user");
|
|
31289
|
+
return textResult(data);
|
|
30946
31290
|
});
|
|
30947
|
-
|
|
31291
|
+
server.registerTool("sw_get_user", {
|
|
30948
31292
|
description: "Get another Splitwise user's profile by id.",
|
|
30949
31293
|
annotations: { readOnlyHint: true },
|
|
30950
31294
|
inputSchema: {
|
|
30951
31295
|
id: external_exports.number().describe("User ID")
|
|
30952
31296
|
}
|
|
30953
31297
|
}, async ({ id }) => {
|
|
30954
|
-
const data = await
|
|
30955
|
-
return
|
|
31298
|
+
const data = await client.request("GET", `/get_user/${id}`);
|
|
31299
|
+
return textResult(data);
|
|
30956
31300
|
});
|
|
30957
|
-
|
|
31301
|
+
server.registerTool("sw_update_user", {
|
|
30958
31302
|
description: "Update the current user's profile fields. id must be the current user's id.",
|
|
30959
31303
|
inputSchema: {
|
|
30960
31304
|
id: external_exports.number().describe("User ID (must be the current user's id)"),
|
|
@@ -30973,31 +31317,31 @@ function registerUserTools(server2, client2) {
|
|
|
30973
31317
|
if (password !== void 0) body.password = password;
|
|
30974
31318
|
if (locale !== void 0) body.locale = locale;
|
|
30975
31319
|
if (default_currency !== void 0) body.default_currency = default_currency;
|
|
30976
|
-
const data = await
|
|
30977
|
-
return
|
|
31320
|
+
const data = await client.request("POST", `/update_user/${id}`, body);
|
|
31321
|
+
return textResult(data);
|
|
30978
31322
|
});
|
|
30979
31323
|
}
|
|
30980
31324
|
|
|
30981
31325
|
// src/tools/groups.ts
|
|
30982
|
-
function registerGroupTools(
|
|
30983
|
-
|
|
31326
|
+
function registerGroupTools(server) {
|
|
31327
|
+
server.registerTool("sw_list_groups", {
|
|
30984
31328
|
description: "List all Splitwise groups the current user belongs to. Returns id, name, and members for each group. Use this to resolve a group name to its id.",
|
|
30985
31329
|
annotations: { readOnlyHint: true }
|
|
30986
31330
|
}, async () => {
|
|
30987
|
-
const data = await
|
|
30988
|
-
return
|
|
31331
|
+
const data = await client.request("GET", "/get_groups");
|
|
31332
|
+
return textResult(data);
|
|
30989
31333
|
});
|
|
30990
|
-
|
|
31334
|
+
server.registerTool("sw_get_group", {
|
|
30991
31335
|
description: "Get details of a single Splitwise group including all members and balances.",
|
|
30992
31336
|
annotations: { readOnlyHint: true },
|
|
30993
31337
|
inputSchema: {
|
|
30994
31338
|
id: external_exports.number().describe("Group ID")
|
|
30995
31339
|
}
|
|
30996
31340
|
}, async ({ id }) => {
|
|
30997
|
-
const data = await
|
|
30998
|
-
return
|
|
31341
|
+
const data = await client.request("GET", `/get_group/${id}`);
|
|
31342
|
+
return textResult(data);
|
|
30999
31343
|
});
|
|
31000
|
-
|
|
31344
|
+
server.registerTool("sw_create_group", {
|
|
31001
31345
|
description: "Create a new Splitwise group.",
|
|
31002
31346
|
inputSchema: {
|
|
31003
31347
|
name: external_exports.string().describe("Group name"),
|
|
@@ -31008,10 +31352,10 @@ function registerGroupTools(server2, client2) {
|
|
|
31008
31352
|
const body = { name };
|
|
31009
31353
|
if (group_type !== void 0) body.group_type = group_type;
|
|
31010
31354
|
if (simplify_by_default !== void 0) body.simplify_by_default = simplify_by_default;
|
|
31011
|
-
const data = await
|
|
31012
|
-
return
|
|
31355
|
+
const data = await client.request("POST", "/create_group", body);
|
|
31356
|
+
return textResult(data);
|
|
31013
31357
|
});
|
|
31014
|
-
|
|
31358
|
+
server.registerTool("sw_add_user_to_group", {
|
|
31015
31359
|
description: "Add a user to a Splitwise group. Provide user_id (preferred, use sw_list_friends to resolve a name) or first_name + last_name + email to invite by email.",
|
|
31016
31360
|
inputSchema: {
|
|
31017
31361
|
group_id: external_exports.number().describe("Group ID"),
|
|
@@ -31030,50 +31374,50 @@ function registerGroupTools(server2, client2) {
|
|
|
31030
31374
|
}
|
|
31031
31375
|
body = { group_id, first_name, last_name, email: email3 };
|
|
31032
31376
|
}
|
|
31033
|
-
const data = await
|
|
31034
|
-
return
|
|
31377
|
+
const data = await client.request("POST", "/add_user_to_group", body);
|
|
31378
|
+
return textResult(data);
|
|
31035
31379
|
});
|
|
31036
|
-
|
|
31380
|
+
server.registerTool("sw_remove_user_from_group", {
|
|
31037
31381
|
description: "Remove a user from a Splitwise group.",
|
|
31038
31382
|
inputSchema: {
|
|
31039
31383
|
group_id: external_exports.number().describe("Group ID"),
|
|
31040
31384
|
user_id: external_exports.number().describe("User ID to remove")
|
|
31041
31385
|
}
|
|
31042
31386
|
}, async ({ group_id, user_id }) => {
|
|
31043
|
-
const data = await
|
|
31044
|
-
return
|
|
31387
|
+
const data = await client.request("POST", "/remove_user_from_group", { group_id, user_id });
|
|
31388
|
+
return textResult(data);
|
|
31045
31389
|
});
|
|
31046
|
-
|
|
31390
|
+
server.registerTool("sw_delete_group", {
|
|
31047
31391
|
description: "Soft-delete a Splitwise group.",
|
|
31048
31392
|
annotations: { destructiveHint: true },
|
|
31049
31393
|
inputSchema: {
|
|
31050
31394
|
id: external_exports.number().describe("Group ID to delete")
|
|
31051
31395
|
}
|
|
31052
31396
|
}, async ({ id }) => {
|
|
31053
|
-
const data = await
|
|
31054
|
-
return
|
|
31397
|
+
const data = await client.request("POST", `/delete_group/${id}`);
|
|
31398
|
+
return textResult(data);
|
|
31055
31399
|
});
|
|
31056
|
-
|
|
31400
|
+
server.registerTool("sw_undelete_group", {
|
|
31057
31401
|
description: "Restore a soft-deleted Splitwise group.",
|
|
31058
31402
|
inputSchema: {
|
|
31059
31403
|
id: external_exports.number().describe("Group ID to restore")
|
|
31060
31404
|
}
|
|
31061
31405
|
}, async ({ id }) => {
|
|
31062
|
-
const data = await
|
|
31063
|
-
return
|
|
31406
|
+
const data = await client.request("POST", `/undelete_group/${id}`);
|
|
31407
|
+
return textResult(data);
|
|
31064
31408
|
});
|
|
31065
31409
|
}
|
|
31066
31410
|
|
|
31067
31411
|
// src/tools/friends.ts
|
|
31068
|
-
function registerFriendTools(
|
|
31069
|
-
|
|
31412
|
+
function registerFriendTools(server) {
|
|
31413
|
+
server.registerTool("sw_list_friends", {
|
|
31070
31414
|
description: "List all Splitwise friends with their id, first_name, last_name, and email. Use this to resolve a friend's name to a user_id before adding them to a group or building a custom expense split.",
|
|
31071
31415
|
annotations: { readOnlyHint: true }
|
|
31072
31416
|
}, async () => {
|
|
31073
|
-
const data = await
|
|
31074
|
-
return
|
|
31417
|
+
const data = await client.request("GET", "/get_friends");
|
|
31418
|
+
return textResult(data);
|
|
31075
31419
|
});
|
|
31076
|
-
|
|
31420
|
+
server.registerTool("sw_create_friend", {
|
|
31077
31421
|
description: "Add a Splitwise friend by email.",
|
|
31078
31422
|
inputSchema: {
|
|
31079
31423
|
user_email: external_exports.string().describe("Email of the user to add as a friend"),
|
|
@@ -31084,18 +31428,18 @@ function registerFriendTools(server2, client2) {
|
|
|
31084
31428
|
const body = { user_email };
|
|
31085
31429
|
if (user_first_name !== void 0) body.user_first_name = user_first_name;
|
|
31086
31430
|
if (user_last_name !== void 0) body.user_last_name = user_last_name;
|
|
31087
|
-
const data = await
|
|
31088
|
-
return
|
|
31431
|
+
const data = await client.request("POST", "/create_friend", body);
|
|
31432
|
+
return textResult(data);
|
|
31089
31433
|
});
|
|
31090
|
-
|
|
31434
|
+
server.registerTool("sw_delete_friend", {
|
|
31091
31435
|
description: "Remove a Splitwise friendship by user id.",
|
|
31092
31436
|
annotations: { destructiveHint: true },
|
|
31093
31437
|
inputSchema: {
|
|
31094
31438
|
id: external_exports.number().describe("User ID of the friend to remove")
|
|
31095
31439
|
}
|
|
31096
31440
|
}, async ({ id }) => {
|
|
31097
|
-
const data = await
|
|
31098
|
-
return
|
|
31441
|
+
const data = await client.request("POST", `/delete_friend/${id}`);
|
|
31442
|
+
return textResult(data);
|
|
31099
31443
|
});
|
|
31100
31444
|
}
|
|
31101
31445
|
|
|
@@ -31127,8 +31471,8 @@ var userShareSchema = external_exports.object({
|
|
|
31127
31471
|
paid_share: external_exports.string().describe('Amount this user paid, e.g. "25.00"'),
|
|
31128
31472
|
owed_share: external_exports.string().describe('Amount this user owes, e.g. "12.50"')
|
|
31129
31473
|
});
|
|
31130
|
-
function registerExpenseTools(
|
|
31131
|
-
|
|
31474
|
+
function registerExpenseTools(server) {
|
|
31475
|
+
server.registerTool("sw_list_expenses", {
|
|
31132
31476
|
description: "List or search Splitwise expenses. All filters are optional. Use group_id to filter by group, dated_after/dated_before for date ranges.",
|
|
31133
31477
|
annotations: { readOnlyHint: true },
|
|
31134
31478
|
inputSchema: {
|
|
@@ -31142,27 +31486,30 @@ function registerExpenseTools(server2, client2) {
|
|
|
31142
31486
|
offset: external_exports.number().describe("Pagination offset").optional()
|
|
31143
31487
|
}
|
|
31144
31488
|
}, async (args) => {
|
|
31145
|
-
const
|
|
31146
|
-
|
|
31147
|
-
|
|
31148
|
-
|
|
31149
|
-
|
|
31150
|
-
|
|
31151
|
-
|
|
31152
|
-
|
|
31153
|
-
|
|
31489
|
+
const qs = buildQueryString({
|
|
31490
|
+
group_id: args.group_id,
|
|
31491
|
+
friend_id: args.friend_id,
|
|
31492
|
+
dated_after: args.dated_after,
|
|
31493
|
+
dated_before: args.dated_before,
|
|
31494
|
+
updated_after: args.updated_after,
|
|
31495
|
+
updated_before: args.updated_before,
|
|
31496
|
+
limit: args.limit,
|
|
31497
|
+
offset: args.offset
|
|
31498
|
+
});
|
|
31499
|
+
const data = await client.request("GET", `/get_expenses${qs}`);
|
|
31500
|
+
return textResult(data);
|
|
31154
31501
|
});
|
|
31155
|
-
|
|
31502
|
+
server.registerTool("sw_get_expense", {
|
|
31156
31503
|
description: "Get full details of a single Splitwise expense by id.",
|
|
31157
31504
|
annotations: { readOnlyHint: true },
|
|
31158
31505
|
inputSchema: {
|
|
31159
31506
|
id: external_exports.number().describe("Expense ID")
|
|
31160
31507
|
}
|
|
31161
31508
|
}, async ({ id }) => {
|
|
31162
|
-
const data = await
|
|
31163
|
-
return
|
|
31509
|
+
const data = await client.request("GET", `/get_expense/${id}`);
|
|
31510
|
+
return textResult(data);
|
|
31164
31511
|
});
|
|
31165
|
-
|
|
31512
|
+
server.registerTool("sw_create_expense", {
|
|
31166
31513
|
description: 'Create a Splitwise expense. Use split_equally:true to split evenly among group members, or provide a users array for custom per-person splits (paid_share and owed_share as decimal strings like "25.00"). cost must be a decimal string.',
|
|
31167
31514
|
inputSchema: {
|
|
31168
31515
|
group_id: external_exports.number().describe("Group to add expense to (use 0 for no group)"),
|
|
@@ -31177,10 +31524,10 @@ function registerExpenseTools(server2, client2) {
|
|
|
31177
31524
|
}
|
|
31178
31525
|
}, async (args) => {
|
|
31179
31526
|
const body = buildExpenseBody(args);
|
|
31180
|
-
const data = await
|
|
31181
|
-
return
|
|
31527
|
+
const data = await client.request("POST", "/create_expense", body);
|
|
31528
|
+
return textResult(data);
|
|
31182
31529
|
});
|
|
31183
|
-
|
|
31530
|
+
server.registerTool("sw_update_expense", {
|
|
31184
31531
|
description: "Edit an existing Splitwise expense. Provide expense_id and any fields to change. For custom split updates, the full users array must be provided (the API replaces the entire split).",
|
|
31185
31532
|
inputSchema: {
|
|
31186
31533
|
expense_id: external_exports.number().describe("ID of the expense to update"),
|
|
@@ -31196,93 +31543,96 @@ function registerExpenseTools(server2, client2) {
|
|
|
31196
31543
|
}, async (args) => {
|
|
31197
31544
|
const { expense_id } = args;
|
|
31198
31545
|
const body = buildExpenseBody(args);
|
|
31199
|
-
const data = await
|
|
31200
|
-
return
|
|
31546
|
+
const data = await client.request("POST", `/update_expense/${expense_id}`, body);
|
|
31547
|
+
return textResult(data);
|
|
31201
31548
|
});
|
|
31202
|
-
|
|
31549
|
+
server.registerTool("sw_delete_expense", {
|
|
31203
31550
|
description: "Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore.",
|
|
31204
31551
|
annotations: { destructiveHint: true },
|
|
31205
31552
|
inputSchema: {
|
|
31206
31553
|
id: external_exports.number().describe("Expense ID to delete")
|
|
31207
31554
|
}
|
|
31208
31555
|
}, async ({ id }) => {
|
|
31209
|
-
const data = await
|
|
31210
|
-
return
|
|
31556
|
+
const data = await client.request("POST", `/delete_expense/${id}`);
|
|
31557
|
+
return textResult(data);
|
|
31211
31558
|
});
|
|
31212
|
-
|
|
31559
|
+
server.registerTool("sw_undelete_expense", {
|
|
31213
31560
|
description: "Restore a soft-deleted Splitwise expense.",
|
|
31214
31561
|
inputSchema: {
|
|
31215
31562
|
id: external_exports.number().describe("Expense ID to restore")
|
|
31216
31563
|
}
|
|
31217
31564
|
}, async ({ id }) => {
|
|
31218
|
-
const data = await
|
|
31219
|
-
return
|
|
31565
|
+
const data = await client.request("POST", `/undelete_expense/${id}`);
|
|
31566
|
+
return textResult(data);
|
|
31220
31567
|
});
|
|
31221
31568
|
}
|
|
31222
31569
|
|
|
31223
31570
|
// src/tools/utilities.ts
|
|
31224
|
-
function registerUtilityTools(
|
|
31225
|
-
|
|
31571
|
+
function registerUtilityTools(server) {
|
|
31572
|
+
server.registerTool("sw_get_notifications", {
|
|
31226
31573
|
description: "Get recent Splitwise activity notifications for the current user.",
|
|
31227
31574
|
annotations: { readOnlyHint: true }
|
|
31228
31575
|
}, async () => {
|
|
31229
|
-
const data = await
|
|
31230
|
-
return
|
|
31576
|
+
const data = await client.request("GET", "/get_notifications");
|
|
31577
|
+
return textResult(data);
|
|
31231
31578
|
});
|
|
31232
|
-
|
|
31579
|
+
server.registerTool("sw_get_categories", {
|
|
31233
31580
|
description: "Get the hierarchical list of Splitwise expense categories. Use the returned id as category_id when creating expenses.",
|
|
31234
31581
|
annotations: { readOnlyHint: true }
|
|
31235
31582
|
}, async () => {
|
|
31236
|
-
const data = await
|
|
31237
|
-
return
|
|
31583
|
+
const data = await client.request("GET", "/get_categories");
|
|
31584
|
+
return textResult(data);
|
|
31238
31585
|
});
|
|
31239
|
-
|
|
31586
|
+
server.registerTool("sw_get_currencies", {
|
|
31240
31587
|
description: "Get all Splitwise-supported currency codes and units. Use the currency_code value when creating expenses in non-default currencies.",
|
|
31241
31588
|
annotations: { readOnlyHint: true }
|
|
31242
31589
|
}, async () => {
|
|
31243
|
-
const data = await
|
|
31244
|
-
return
|
|
31590
|
+
const data = await client.request("GET", "/get_currencies");
|
|
31591
|
+
return textResult(data);
|
|
31245
31592
|
});
|
|
31246
|
-
|
|
31593
|
+
server.registerTool("sw_get_comments", {
|
|
31247
31594
|
description: "Get all comments on a Splitwise expense.",
|
|
31248
31595
|
annotations: { readOnlyHint: true },
|
|
31249
31596
|
inputSchema: {
|
|
31250
31597
|
expense_id: external_exports.number().describe("Expense ID to get comments for")
|
|
31251
31598
|
}
|
|
31252
31599
|
}, async ({ expense_id }) => {
|
|
31253
|
-
const data = await
|
|
31254
|
-
return
|
|
31600
|
+
const data = await client.request("GET", `/get_comments${buildQueryString({ expense_id })}`);
|
|
31601
|
+
return textResult(data);
|
|
31255
31602
|
});
|
|
31256
|
-
|
|
31603
|
+
server.registerTool("sw_create_comment", {
|
|
31257
31604
|
description: "Add a comment to a Splitwise expense.",
|
|
31258
31605
|
inputSchema: {
|
|
31259
31606
|
expense_id: external_exports.number().describe("Expense ID to comment on"),
|
|
31260
31607
|
content: external_exports.string().describe("Comment text")
|
|
31261
31608
|
}
|
|
31262
31609
|
}, async ({ expense_id, content }) => {
|
|
31263
|
-
const data = await
|
|
31264
|
-
return
|
|
31610
|
+
const data = await client.request("POST", "/create_comment", { expense_id, content });
|
|
31611
|
+
return textResult(data);
|
|
31265
31612
|
});
|
|
31266
|
-
|
|
31613
|
+
server.registerTool("sw_delete_comment", {
|
|
31267
31614
|
description: "Delete a comment by id.",
|
|
31268
31615
|
annotations: { destructiveHint: true },
|
|
31269
31616
|
inputSchema: {
|
|
31270
31617
|
id: external_exports.number().describe("Comment ID to delete")
|
|
31271
31618
|
}
|
|
31272
31619
|
}, async ({ id }) => {
|
|
31273
|
-
const data = await
|
|
31274
|
-
return
|
|
31620
|
+
const data = await client.request("POST", `/delete_comment/${id}`);
|
|
31621
|
+
return textResult(data);
|
|
31275
31622
|
});
|
|
31276
31623
|
}
|
|
31277
31624
|
|
|
31278
31625
|
// src/index.ts
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
|
|
31626
|
+
await runMcp({
|
|
31627
|
+
name: "splitwise-mcp",
|
|
31628
|
+
version: "2.0.9",
|
|
31629
|
+
// x-release-please-version
|
|
31630
|
+
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
|
31631
|
+
tools: [
|
|
31632
|
+
registerUserTools,
|
|
31633
|
+
registerGroupTools,
|
|
31634
|
+
registerFriendTools,
|
|
31635
|
+
registerExpenseTools,
|
|
31636
|
+
registerUtilityTools
|
|
31637
|
+
]
|
|
31638
|
+
});
|