splitwise-mcp 2.0.8 → 2.0.9

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