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/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
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
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
- function normalizeComponentEncoding(component, esc2) {
3301
- const func = esc2 !== true ? escape : unescape;
3302
- if (component.scheme !== void 0) {
3303
- component.scheme = func(component.scheme);
3304
- }
3305
- if (component.userinfo !== void 0) {
3306
- component.userinfo = func(component.userinfo);
3307
- }
3308
- if (component.host !== void 0) {
3309
- component.host = func(component.host);
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
- if (component.path !== void 0) {
3312
- component.path = func(component.path);
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
- if (component.query !== void 0) {
3315
- component.query = func(component.query);
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
- if (component.fragment !== void 0) {
3318
- component.fragment = func(component.fragment);
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 component;
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 = component.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
- normalizeComponentEncoding,
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, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
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
- serialize(parse3(uri, options), options);
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
- if (typeof uriA === "string") {
3646
- uriA = unescape(uriA);
3647
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
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 = escape(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 = unescape(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 parse3(uri, opts) {
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 = escape(unescape(parsed.path));
3860
+ parsed.path = normalizePathEncoding(parsed.path);
3796
3861
  }
3797
3862
  if (parsed.fragment) {
3798
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
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(transport2) {
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 = transport2;
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(transport2) {
30014
- return await this.server.connect(transport2);
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
- try {
30868
- const { config: config2 } = await import("dotenv");
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 = readVar("SPLITWISE_API_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.doRequest(method, path, body, false);
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(server2, client2) {
30940
- server2.registerTool("sw_get_current_user", {
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 client2.request("GET", "/get_current_user");
30945
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31294
+ const data = await client.request("GET", "/get_current_user");
31295
+ return textResult(data);
30946
31296
  });
30947
- server2.registerTool("sw_get_user", {
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 client2.request("GET", `/get_user/${id}`);
30955
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31304
+ const data = await client.request("GET", `/get_user/${id}`);
31305
+ return textResult(data);
30956
31306
  });
30957
- server2.registerTool("sw_update_user", {
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 client2.request("POST", `/update_user/${id}`, body);
30977
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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(server2, client2) {
30983
- server2.registerTool("sw_list_groups", {
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 client2.request("GET", "/get_groups");
30988
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31337
+ const data = await client.request("GET", "/get_groups");
31338
+ return textResult(data);
30989
31339
  });
30990
- server2.registerTool("sw_get_group", {
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 client2.request("GET", `/get_group/${id}`);
30998
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31347
+ const data = await client.request("GET", `/get_group/${id}`);
31348
+ return textResult(data);
30999
31349
  });
31000
- server2.registerTool("sw_create_group", {
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 client2.request("POST", "/create_group", body);
31012
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31361
+ const data = await client.request("POST", "/create_group", body);
31362
+ return textResult(data);
31013
31363
  });
31014
- server2.registerTool("sw_add_user_to_group", {
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 client2.request("POST", "/add_user_to_group", body);
31034
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31383
+ const data = await client.request("POST", "/add_user_to_group", body);
31384
+ return textResult(data);
31035
31385
  });
31036
- server2.registerTool("sw_remove_user_from_group", {
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 client2.request("POST", "/remove_user_from_group", { group_id, user_id });
31044
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31393
+ const data = await client.request("POST", "/remove_user_from_group", { group_id, user_id });
31394
+ return textResult(data);
31045
31395
  });
31046
- server2.registerTool("sw_delete_group", {
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 client2.request("POST", `/delete_group/${id}`);
31054
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31403
+ const data = await client.request("POST", `/delete_group/${id}`);
31404
+ return textResult(data);
31055
31405
  });
31056
- server2.registerTool("sw_undelete_group", {
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 client2.request("POST", `/undelete_group/${id}`);
31063
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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(server2, client2) {
31069
- server2.registerTool("sw_list_friends", {
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 client2.request("GET", "/get_friends");
31074
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31423
+ const data = await client.request("GET", "/get_friends");
31424
+ return textResult(data);
31075
31425
  });
31076
- server2.registerTool("sw_create_friend", {
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 client2.request("POST", "/create_friend", body);
31088
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31437
+ const data = await client.request("POST", "/create_friend", body);
31438
+ return textResult(data);
31089
31439
  });
31090
- server2.registerTool("sw_delete_friend", {
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 client2.request("POST", `/delete_friend/${id}`);
31098
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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(server2, client2) {
31131
- server2.registerTool("sw_list_expenses", {
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 params = new URLSearchParams();
31146
- const filters = ["group_id", "friend_id", "dated_after", "dated_before", "updated_after", "updated_before", "limit", "offset"];
31147
- for (const key of filters) {
31148
- const val = args[key];
31149
- if (val !== void 0) params.append(key, String(val));
31150
- }
31151
- const qs = params.toString();
31152
- const data = await client2.request("GET", qs ? `/get_expenses?${qs}` : "/get_expenses");
31153
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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
- server2.registerTool("sw_get_expense", {
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 client2.request("GET", `/get_expense/${id}`);
31163
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31515
+ const data = await client.request("GET", `/get_expense/${id}`);
31516
+ return textResult(data);
31164
31517
  });
31165
- server2.registerTool("sw_create_expense", {
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 client2.request("POST", "/create_expense", body);
31181
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31533
+ const data = await client.request("POST", "/create_expense", body);
31534
+ return textResult(data);
31182
31535
  });
31183
- server2.registerTool("sw_update_expense", {
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 client2.request("POST", `/update_expense/${expense_id}`, body);
31200
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31552
+ const data = await client.request("POST", `/update_expense/${expense_id}`, body);
31553
+ return textResult(data);
31201
31554
  });
31202
- server2.registerTool("sw_delete_expense", {
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 client2.request("POST", `/delete_expense/${id}`);
31210
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31562
+ const data = await client.request("POST", `/delete_expense/${id}`);
31563
+ return textResult(data);
31211
31564
  });
31212
- server2.registerTool("sw_undelete_expense", {
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 client2.request("POST", `/undelete_expense/${id}`);
31219
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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(server2, client2) {
31225
- server2.registerTool("sw_get_notifications", {
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 client2.request("GET", "/get_notifications");
31230
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31582
+ const data = await client.request("GET", "/get_notifications");
31583
+ return textResult(data);
31231
31584
  });
31232
- server2.registerTool("sw_get_categories", {
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 client2.request("GET", "/get_categories");
31237
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31589
+ const data = await client.request("GET", "/get_categories");
31590
+ return textResult(data);
31238
31591
  });
31239
- server2.registerTool("sw_get_currencies", {
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 client2.request("GET", "/get_currencies");
31244
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31596
+ const data = await client.request("GET", "/get_currencies");
31597
+ return textResult(data);
31245
31598
  });
31246
- server2.registerTool("sw_get_comments", {
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 client2.request("GET", `/get_comments?expense_id=${expense_id}`);
31254
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31606
+ const data = await client.request("GET", `/get_comments${buildQueryString({ expense_id })}`);
31607
+ return textResult(data);
31255
31608
  });
31256
- server2.registerTool("sw_create_comment", {
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 client2.request("POST", "/create_comment", { expense_id, content });
31264
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
31616
+ const data = await client.request("POST", "/create_comment", { expense_id, content });
31617
+ return textResult(data);
31265
31618
  });
31266
- server2.registerTool("sw_delete_comment", {
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 client2.request("POST", `/delete_comment/${id}`);
31274
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
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
- var client = new SplitwiseClient();
31280
- var server = new McpServer({ name: "splitwise-mcp", version: "2.0.8" });
31281
- registerUserTools(server, client);
31282
- registerGroupTools(server, client);
31283
- registerFriendTools(server, client);
31284
- registerExpenseTools(server, client);
31285
- registerUtilityTools(server, client);
31286
- console.error("[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.");
31287
- var transport = new StdioServerTransport();
31288
- await server.connect(transport);
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
+ });