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