splitwise-mcp 3.0.0 → 3.1.0

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Splitwise — natural-language expense and group management via the Splitwise API",
10
- "version": "3.0.0"
10
+ "version": "3.1.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Splitwise",
16
16
  "source": "./",
17
17
  "description": "MCP server for Splitwise — manage expenses, groups, and friends via natural language",
18
- "version": "3.0.0",
18
+ "version": "3.1.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
3
  "displayName": "Splitwise",
4
- "version": "3.0.0",
4
+ "version": "3.1.0",
5
5
  "description": "MCP server for Splitwise — natural-language expense and group management via the Splitwise API",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/README.md CHANGED
@@ -25,7 +25,7 @@ Ask Claude things like:
25
25
  ## Requirements
26
26
 
27
27
  - [Claude Desktop](https://claude.ai/download) or [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
28
- - [Node.js](https://nodejs.org) 20.6 or later
28
+ - [Node.js](https://nodejs.org) 22 or later
29
29
  - A [Splitwise](https://www.splitwise.com) account and API key
30
30
 
31
31
  ## Acknowledgement of Terms
package/dist/bundle.js CHANGED
@@ -64373,6 +64373,7 @@ function missingClientCapabilities(required2, declared) {
64373
64373
  return Object.keys(missing).length > 0 ? missing : void 0;
64374
64374
  }
64375
64375
  var FIRST_MODERN_PROTOCOL_VERSION = "2026-07-28";
64376
+ var SUPPORTED_MODERN_PROTOCOL_VERSIONS = [FIRST_MODERN_PROTOCOL_VERSION];
64376
64377
  function isModernProtocolVersion(version2) {
64377
64378
  return version2 >= FIRST_MODERN_PROTOCOL_VERSION;
64378
64379
  }
@@ -66931,6 +66932,25 @@ function isSpecNotificationMethod(method) {
66931
66932
  return ALL_CODECS.some((codec2) => codec2.hasNotificationMethod(method));
66932
66933
  }
66933
66934
  var ALL_CODECS = [rev2025Codec, rev2026Codec];
66935
+ function isPlainObject$3(value) {
66936
+ return value !== null && typeof value === "object" && !Array.isArray(value);
66937
+ }
66938
+ function requestMetaOf(params) {
66939
+ if (!isPlainObject$3(params)) return void 0;
66940
+ const meta3 = params["_meta"];
66941
+ return isPlainObject$3(meta3) ? meta3 : void 0;
66942
+ }
66943
+ function hasEnvelopeClaim(params) {
66944
+ const meta3 = requestMetaOf(params);
66945
+ return meta3 !== void 0 && PROTOCOL_VERSION_META_KEY in meta3;
66946
+ }
66947
+ function envelopeClaimVersion(params) {
66948
+ const value = requestMetaOf(params)?.[PROTOCOL_VERSION_META_KEY];
66949
+ return typeof value === "string" ? value : void 0;
66950
+ }
66951
+ function validateEnvelopeMeta(meta3) {
66952
+ return codecForVersion(MODERN_WIRE_REVISION).validateEnvelopeMeta(meta3);
66953
+ }
66934
66954
  var schemas_exports = /* @__PURE__ */ __exportAll({
66935
66955
  AnnotationsSchema: () => AnnotationsSchema,
66936
66956
  AudioContentSchema: () => AudioContentSchema,
@@ -67281,6 +67301,45 @@ var LADDER_ERROR_HTTP_STATUS = {
67281
67301
  [ProtocolErrorCode.MissingRequiredClientCapability]: 400,
67282
67302
  [HEADER_MISMATCH_ERROR_CODE]: 400
67283
67303
  };
67304
+ function rejection(rung, cell, httpStatus, error62, settled) {
67305
+ return {
67306
+ kind: "reject",
67307
+ rung,
67308
+ cell,
67309
+ httpStatus,
67310
+ code: error62.code,
67311
+ message: error62.message,
67312
+ ...error62.data !== void 0 && { data: error62.data },
67313
+ settled
67314
+ };
67315
+ }
67316
+ function carriesValidModernEnvelopeClaim(params) {
67317
+ if (!hasEnvelopeClaim(params)) return false;
67318
+ const claimedVersion = envelopeClaimVersion(params);
67319
+ if (claimedVersion === void 0 || !isModernProtocolVersion(claimedVersion)) return false;
67320
+ const meta3 = requestMetaOf(params);
67321
+ return meta3 !== void 0 && validateEnvelopeMeta(meta3).length === 0;
67322
+ }
67323
+ function modernOnlyStrictRejection(route, supportedVersions) {
67324
+ switch (route.reason) {
67325
+ case "http-method":
67326
+ return rejection("http-method", "modern-only-method-not-allowed", 405, new ProtocolError(-32e3, "Method not allowed."), true);
67327
+ case "batch":
67328
+ return rejection("jsonrpc-shape", "modern-only-batch-not-supported", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC batches are not supported by this endpoint"), true);
67329
+ case "response":
67330
+ return rejection("jsonrpc-shape", "modern-only-response-post", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC responses cannot be posted to this endpoint"), true);
67331
+ case "notification":
67332
+ return;
67333
+ case "initialize":
67334
+ case "no-claim": {
67335
+ const requested = route.requestedVersion;
67336
+ return rejection("era-classification", "modern-only-missing-envelope", 400, requested === void 0 ? new ProtocolError(ProtocolErrorCode.UnsupportedProtocolVersion, "Unsupported protocol version: the request did not name a protocol version", { supported: [...supportedVersions] }) : new UnsupportedProtocolVersionError({
67337
+ supported: [...supportedVersions],
67338
+ requested
67339
+ }), true);
67340
+ }
67341
+ }
67342
+ }
67284
67343
  function parseSchema(schema, data) {
67285
67344
  return z2.safeParse(schema, data);
67286
67345
  }
@@ -67842,6 +67901,9 @@ function withRequestStateValue(ctx, value) {
67842
67901
  };
67843
67902
  }
67844
67903
  var writeNegotiatedProtocolVersion;
67904
+ function setNegotiatedProtocolVersion(instance, version2) {
67905
+ writeNegotiatedProtocolVersion(instance, version2);
67906
+ }
67845
67907
  var Protocol = class {
67846
67908
  _transport;
67847
67909
  _requestMessageId = 0;
@@ -75785,6 +75847,190 @@ function getCompleter(schema) {
75785
75847
  return schema[COMPLETABLE_SYMBOL]?.complete;
75786
75848
  }
75787
75849
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
75850
+ function listenFilterAccepts(filter, event) {
75851
+ switch (event.kind) {
75852
+ case "tools_list_changed":
75853
+ return filter.toolsListChanged === true;
75854
+ case "prompts_list_changed":
75855
+ return filter.promptsListChanged === true;
75856
+ case "resources_list_changed":
75857
+ return filter.resourcesListChanged === true;
75858
+ case "resource_updated":
75859
+ return filter.resourceSubscriptions !== void 0 && filter.resourceSubscriptions.includes(event.uri);
75860
+ }
75861
+ }
75862
+ function honoredSubset(requested, capabilities) {
75863
+ const honored = {};
75864
+ const allow = (bit) => capabilities === void 0 || bit === true;
75865
+ if (requested.toolsListChanged === true && allow(capabilities?.tools?.listChanged)) honored.toolsListChanged = true;
75866
+ if (requested.promptsListChanged === true && allow(capabilities?.prompts?.listChanged)) honored.promptsListChanged = true;
75867
+ if (requested.resourcesListChanged === true && allow(capabilities?.resources?.listChanged)) honored.resourcesListChanged = true;
75868
+ if (requested.resourceSubscriptions !== void 0 && requested.resourceSubscriptions.length > 0 && allow(capabilities?.resources?.subscribe)) honored.resourceSubscriptions = [...requested.resourceSubscriptions];
75869
+ return honored;
75870
+ }
75871
+ var DEFAULT_MAX_SUBSCRIPTIONS = 1024;
75872
+ function stampSubscriptionId(notification, subscriptionId) {
75873
+ return {
75874
+ method: notification.method,
75875
+ params: {
75876
+ ...notification.params,
75877
+ _meta: {
75878
+ ...notification.params?._meta,
75879
+ [SUBSCRIPTION_ID_META_KEY]: subscriptionId
75880
+ }
75881
+ }
75882
+ };
75883
+ }
75884
+ function parseListenFilter(message) {
75885
+ const outcome = codecForVersion(MODERN_WIRE_REVISION).validateRequest("subscriptions/listen", message);
75886
+ return outcome.ok ? outcome.value.params?.notifications : void 0;
75887
+ }
75888
+ var CHANGE_NOTIFICATION_METHODS = /* @__PURE__ */ new Set([
75889
+ "notifications/tools/list_changed",
75890
+ "notifications/prompts/list_changed",
75891
+ "notifications/resources/list_changed",
75892
+ "notifications/resources/updated"
75893
+ ]);
75894
+ var StdioListenRouter = class {
75895
+ /** Active subscriptions, keyed by the listen request's JSON-RPC id verbatim. */
75896
+ _subs = /* @__PURE__ */ new Map();
75897
+ /**
75898
+ * The serving instance's declared capabilities. Filled in by the entry
75899
+ * once the modern instance is constructed (the router is created before
75900
+ * the instance exists), so the acknowledged filter is narrowed against
75901
+ * what the server can actually deliver.
75902
+ */
75903
+ _serverCapabilities;
75904
+ /**
75905
+ * The serving instance's identity, stamped onto the graceful-close
75906
+ * results' `_meta` (the spec's `SubscriptionsListenResultMeta` extends
75907
+ * `ResultMetaObject`). Handed over together with the capabilities.
75908
+ */
75909
+ _serverInfo;
75910
+ constructor(_maxSubscriptions = DEFAULT_MAX_SUBSCRIPTIONS, serverCapabilities, serverInfo) {
75911
+ this._maxSubscriptions = _maxSubscriptions;
75912
+ this._serverCapabilities = serverCapabilities;
75913
+ this._serverInfo = serverInfo;
75914
+ }
75915
+ /**
75916
+ * Record the serving instance's declared capabilities and identity once
75917
+ * it has been constructed. Called by `serveStdio`'s connect path;
75918
+ * subsequent `serve()` calls narrow the honored filter against the
75919
+ * capabilities, and `teardownAll()` stamps the identity.
75920
+ */
75921
+ setServerCapabilities(capabilities, serverInfo) {
75922
+ this._serverCapabilities = capabilities;
75923
+ if (serverInfo !== void 0) this._serverInfo = serverInfo;
75924
+ }
75925
+ /** Whether `id` is an active listen subscription on this connection. */
75926
+ has(id) {
75927
+ return this._subs.has(id);
75928
+ }
75929
+ /**
75930
+ * Serve one inbound `subscriptions/listen` request: registers the
75931
+ * subscription and returns the stamped acknowledged notification (or, on
75932
+ * capacity / params rejection, the in-band JSON-RPC error response).
75933
+ *
75934
+ * @throws when called before {@linkcode setServerCapabilities} (or the
75935
+ * constructor) has supplied the serving instance's capabilities. Honoring a
75936
+ * filter without knowing the server's advertised capabilities would fail
75937
+ * open (deliver unadvertised types); the entry guarantees capabilities are
75938
+ * set before any listen request is routed here.
75939
+ */
75940
+ serve(message) {
75941
+ if (this._serverCapabilities === void 0) throw new Error("StdioListenRouter.serve() called before setServerCapabilities(); refusing to honor a filter without capabilities");
75942
+ if (this._subs.size >= this._maxSubscriptions) return {
75943
+ jsonrpc: "2.0",
75944
+ id: message.id,
75945
+ error: {
75946
+ code: -32603,
75947
+ message: "Subscription limit reached"
75948
+ }
75949
+ };
75950
+ const filter = parseListenFilter(message);
75951
+ if (filter === void 0) return {
75952
+ jsonrpc: "2.0",
75953
+ id: message.id,
75954
+ error: {
75955
+ code: -32602,
75956
+ message: "Invalid params: 'notifications' is required and must be a valid SubscriptionFilter"
75957
+ }
75958
+ };
75959
+ const honored = honoredSubset(filter, this._serverCapabilities);
75960
+ this._subs.set(message.id, honored);
75961
+ return stampSubscriptionId({
75962
+ method: "notifications/subscriptions/acknowledged",
75963
+ params: { notifications: honored }
75964
+ }, message.id);
75965
+ }
75966
+ /**
75967
+ * Tear down one subscription (inbound `notifications/cancelled`). Returns
75968
+ * `true` when a subscription was removed. After this call NOTHING further
75969
+ * is delivered for that subscription id (the post-cancel hardening).
75970
+ */
75971
+ cancel(id) {
75972
+ return this._subs.delete(id);
75973
+ }
75974
+ /**
75975
+ * Route an outbound notification through the active subscriptions.
75976
+ *
75977
+ * - For a subscription-gated change notification, returns one stamped copy
75978
+ * per subscription that opted in to it (an empty array means it is
75979
+ * dropped — the modern era never delivers an un-requested change type).
75980
+ * - For any other outbound message, returns `'passthrough'` (the entry
75981
+ * forwards it as-is).
75982
+ */
75983
+ routeOutbound(message) {
75984
+ if (!CHANGE_NOTIFICATION_METHODS.has(message.method)) return "passthrough";
75985
+ const uriParam = message.params?.["uri"];
75986
+ const uri = typeof uriParam === "string" ? uriParam : void 0;
75987
+ const event = notificationToServerEvent(message.method, uri);
75988
+ const out = [];
75989
+ for (const [subscriptionId, filter] of this._subs) if (listenFilterAccepts(filter, event)) out.push(stampSubscriptionId({
75990
+ method: message.method,
75991
+ params: message.params ?? {}
75992
+ }, subscriptionId));
75993
+ return out;
75994
+ }
75995
+ /**
75996
+ * Server-side graceful teardown of every active subscription: returns the
75997
+ * empty `subscriptions/listen` JSON-RPC result for each subscription id —
75998
+ * the spec's graceful-close signal, `_meta` carrying the subscription id
75999
+ * and the serving instance's identity — for the entry to emit before
76000
+ * closing the wire. Clears the set so nothing further is delivered.
76001
+ */
76002
+ teardownAll() {
76003
+ const out = [];
76004
+ for (const id of this._subs.keys()) out.push({
76005
+ jsonrpc: "2.0",
76006
+ id,
76007
+ result: {
76008
+ resultType: "complete",
76009
+ _meta: {
76010
+ [SUBSCRIPTION_ID_META_KEY]: id,
76011
+ ...this._serverInfo !== void 0 && { [SERVER_INFO_META_KEY]: this._serverInfo }
76012
+ }
76013
+ }
76014
+ });
76015
+ this._subs.clear();
76016
+ return out;
76017
+ }
76018
+ };
76019
+ function notificationToServerEvent(method, uri) {
76020
+ switch (method) {
76021
+ case "notifications/tools/list_changed":
76022
+ return { kind: "tools_list_changed" };
76023
+ case "notifications/prompts/list_changed":
76024
+ return { kind: "prompts_list_changed" };
76025
+ case "notifications/resources/list_changed":
76026
+ return { kind: "resources_list_changed" };
76027
+ default:
76028
+ return {
76029
+ kind: "resource_updated",
76030
+ uri: uri ?? ""
76031
+ };
76032
+ }
76033
+ }
75788
76034
  var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
75789
76035
  var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
75790
76036
  function resolveLegacyShimOptions(options) {
@@ -75922,6 +76168,12 @@ var INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
75922
76168
  var writeClientIdentity;
75923
76169
  var installDiscoverHandler;
75924
76170
  var readServerIdentity;
76171
+ function installModernOnlyHandlers(server, servedModernVersions) {
76172
+ installDiscoverHandler(server, servedModernVersions);
76173
+ }
76174
+ function serverIdentityOf(server) {
76175
+ return readServerIdentity(server);
76176
+ }
75925
76177
  var Server = class extends Protocol {
75926
76178
  _clientCapabilities;
75927
76179
  _clientVersion;
@@ -77196,6 +77448,400 @@ var StdioServerTransport = class {
77196
77448
  });
77197
77449
  }
77198
77450
  };
77451
+ var DISCARD_ANSWER_TIMEOUT_MS = 3e3;
77452
+ var StdioConnectionChannel = class {
77453
+ onclose;
77454
+ onerror;
77455
+ onmessage;
77456
+ _closed = false;
77457
+ /** Request ids the entry delivered to the instance that the instance has not yet answered. */
77458
+ _pendingRequests = /* @__PURE__ */ new Set();
77459
+ _drainWaiters = [];
77460
+ constructor(_wire, _onInstanceClose, _outboundIntercept) {
77461
+ this._wire = _wire;
77462
+ this._onInstanceClose = _onInstanceClose;
77463
+ this._outboundIntercept = _outboundIntercept;
77464
+ }
77465
+ async start() {
77466
+ }
77467
+ async send(message, options) {
77468
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
77469
+ const { id } = message;
77470
+ if (id !== void 0) this._settle(id);
77471
+ }
77472
+ if (this._closed) return;
77473
+ if (this._outboundIntercept?.(message) === "handled") return;
77474
+ return this._wire.send(message, options);
77475
+ }
77476
+ setProtocolVersion = (version2) => {
77477
+ this._wire.setProtocolVersion?.(version2);
77478
+ };
77479
+ /** Forwards one inbound message to the connected instance. */
77480
+ deliver(message, extra) {
77481
+ if (this._closed) return;
77482
+ if (isJSONRPCRequest(message)) this._pendingRequests.add(message.id);
77483
+ else if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
77484
+ const cancelledId = message.params?.requestId;
77485
+ if (cancelledId !== void 0) this._settle(cancelledId);
77486
+ }
77487
+ this.onmessage?.(message, extra);
77488
+ }
77489
+ /**
77490
+ * Resolves once every request delivered to the instance has been answered
77491
+ * through {@linkcode send}, settled by a delivered cancellation, or the
77492
+ * channel has been closed and nothing further can be answered. The wait is
77493
+ * bounded by `timeoutMs` as a backstop so no edge can hold the caller
77494
+ * indefinitely; resolves `false` only when the bound elapsed with requests
77495
+ * still unanswered. Used by the probe-discard path so a probe request the
77496
+ * entry accepted is never silently dropped.
77497
+ */
77498
+ async whenRequestsAnswered(timeoutMs) {
77499
+ if (this._closed || this._pendingRequests.size === 0) return true;
77500
+ return await new Promise((resolve2) => {
77501
+ const waiter = () => {
77502
+ clearTimeout(timer);
77503
+ resolve2(true);
77504
+ };
77505
+ const timer = setTimeout(() => {
77506
+ this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
77507
+ resolve2(false);
77508
+ }, timeoutMs);
77509
+ this._drainWaiters.push(waiter);
77510
+ });
77511
+ }
77512
+ async close() {
77513
+ if (this._closed) return;
77514
+ this._closed = true;
77515
+ this._pendingRequests.clear();
77516
+ this._releaseDrainWaiters();
77517
+ try {
77518
+ this._onInstanceClose();
77519
+ } finally {
77520
+ this.onclose?.();
77521
+ }
77522
+ }
77523
+ _settle(id) {
77524
+ this._pendingRequests.delete(id);
77525
+ if (this._pendingRequests.size === 0) this._releaseDrainWaiters();
77526
+ }
77527
+ _releaseDrainWaiters() {
77528
+ const waiters = this._drainWaiters;
77529
+ this._drainWaiters = [];
77530
+ for (const waiter of waiters) waiter();
77531
+ }
77532
+ };
77533
+ function classifyOpeningMessage(message) {
77534
+ const params = message.params;
77535
+ if (message.method === "initialize" && !carriesValidModernEnvelopeClaim(params)) {
77536
+ const requestedVersion = params !== null && typeof params === "object" && typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
77537
+ return {
77538
+ kind: "legacy",
77539
+ reason: "initialize",
77540
+ ...requestedVersion !== void 0 && { requestedVersion }
77541
+ };
77542
+ }
77543
+ if (!hasEnvelopeClaim(params)) return {
77544
+ kind: "legacy",
77545
+ reason: "no-claim"
77546
+ };
77547
+ const meta3 = requestMetaOf(params);
77548
+ const firstIssue = (meta3 === void 0 ? [] : validateEnvelopeMeta(meta3))[0];
77549
+ if (firstIssue !== void 0) return {
77550
+ kind: "invalid-envelope",
77551
+ issue: firstIssue
77552
+ };
77553
+ const claimedVersion = envelopeClaimVersion(params);
77554
+ if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) return {
77555
+ kind: "unsupported-revision",
77556
+ requested: claimedVersion ?? "unknown"
77557
+ };
77558
+ return {
77559
+ kind: "modern",
77560
+ revision: claimedVersion,
77561
+ classification: {
77562
+ era: "modern",
77563
+ revision: claimedVersion
77564
+ }
77565
+ };
77566
+ }
77567
+ function serveStdio(factory, options = {}) {
77568
+ const legacyMode = options.legacy ?? "serve";
77569
+ const wire = options.transport ?? new StdioServerTransport();
77570
+ let state = { phase: "opening" };
77571
+ let discarding;
77572
+ let closing = false;
77573
+ const isTornDown = () => closing || state.phase === "closed";
77574
+ const reportError = (error62) => {
77575
+ try {
77576
+ options.onerror?.(error62);
77577
+ } catch {
77578
+ }
77579
+ };
77580
+ const writeErrorResponse = (id, code, message, data) => wire.send({
77581
+ jsonrpc: "2.0",
77582
+ id,
77583
+ error: {
77584
+ code,
77585
+ message,
77586
+ ...data !== void 0 && { data }
77587
+ }
77588
+ }).catch((error62) => reportError(toError(error62)));
77589
+ const listenRouter = new StdioListenRouter(options.maxSubscriptions ?? DEFAULT_MAX_SUBSCRIPTIONS);
77590
+ const modernOutboundIntercept = (message) => {
77591
+ if (!isJSONRPCNotification(message)) return void 0;
77592
+ const routed = listenRouter.routeOutbound(message);
77593
+ if (routed === "passthrough") return void 0;
77594
+ for (const stamped of routed) wire.send({
77595
+ jsonrpc: "2.0",
77596
+ ...stamped
77597
+ }).catch((error62) => reportError(toError(error62)));
77598
+ return "handled";
77599
+ };
77600
+ const tryServeListen = async (message) => {
77601
+ if (isJSONRPCRequest(message) && message.method === "subscriptions/listen") {
77602
+ const meta3 = requestMetaOf(message.params);
77603
+ const issue2 = hasEnvelopeClaim(message.params) ? (meta3 === void 0 ? [] : validateEnvelopeMeta(meta3))[0] : {
77604
+ key: "_meta",
77605
+ problem: "the per-request envelope is required on protocol revision 2026-07-28"
77606
+ };
77607
+ const claimedVersion = envelopeClaimVersion(message.params);
77608
+ let reply;
77609
+ if (issue2 !== void 0) reply = {
77610
+ jsonrpc: "2.0",
77611
+ id: message.id,
77612
+ error: {
77613
+ code: -32602,
77614
+ message: `Invalid _meta envelope: ${issue2.key}: ${issue2.problem}`
77615
+ }
77616
+ };
77617
+ else if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) {
77618
+ const error62 = new UnsupportedProtocolVersionError({
77619
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
77620
+ requested: claimedVersion ?? "unknown"
77621
+ });
77622
+ reply = {
77623
+ jsonrpc: "2.0",
77624
+ id: message.id,
77625
+ error: {
77626
+ code: error62.code,
77627
+ message: error62.message,
77628
+ data: error62.data
77629
+ }
77630
+ };
77631
+ } else reply = listenRouter.serve(message);
77632
+ await wire.send("error" in reply ? reply : {
77633
+ jsonrpc: "2.0",
77634
+ method: reply.method,
77635
+ params: reply.params
77636
+ }).catch((error62) => reportError(toError(error62)));
77637
+ return true;
77638
+ }
77639
+ if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
77640
+ const cancelledId = message.params?.requestId;
77641
+ if (cancelledId !== void 0 && listenRouter.cancel(cancelledId)) return true;
77642
+ }
77643
+ return false;
77644
+ };
77645
+ const answerLegacyRejection = (request, reason, requestedVersion) => {
77646
+ const rejection2 = modernOnlyStrictRejection({
77647
+ kind: "legacy",
77648
+ reason,
77649
+ ...requestedVersion !== void 0 && { requestedVersion }
77650
+ }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
77651
+ if (rejection2 === void 0) return Promise.resolve();
77652
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
77653
+ return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
77654
+ };
77655
+ const onInstanceClosed = (channel) => {
77656
+ if (closing || channel === discarding) return;
77657
+ closeAll();
77658
+ };
77659
+ const connectInstance = async (era, revision) => {
77660
+ const product = await factory({ era });
77661
+ const server = product instanceof McpServer ? product.server : product;
77662
+ if (era === "modern") {
77663
+ setNegotiatedProtocolVersion(server, revision);
77664
+ installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
77665
+ listenRouter.setServerCapabilities(server.getCapabilities(), serverIdentityOf(server));
77666
+ }
77667
+ const channel = new StdioConnectionChannel(wire, () => onInstanceClosed(channel), era === "modern" ? modernOutboundIntercept : void 0);
77668
+ await product.connect(channel);
77669
+ return {
77670
+ product,
77671
+ channel
77672
+ };
77673
+ };
77674
+ const disposeLateInstance = (instance) => instance.product.close().catch((error62) => reportError(toError(error62)));
77675
+ const discardProbeInstance = async (instance) => {
77676
+ discarding = instance.channel;
77677
+ try {
77678
+ if (!await instance.channel.whenRequestsAnswered(DISCARD_ANSWER_TIMEOUT_MS)) reportError(/* @__PURE__ */ new Error(`Discarded the probe instance with requests still unanswered after ${DISCARD_ANSWER_TIMEOUT_MS}ms; continuing with the fallback`));
77679
+ await instance.product.close();
77680
+ } catch (error62) {
77681
+ reportError(toError(error62));
77682
+ } finally {
77683
+ discarding = void 0;
77684
+ }
77685
+ };
77686
+ const processMessage = async (message) => {
77687
+ if (state.phase === "closed") return;
77688
+ if (state.phase === "pinned") {
77689
+ if (state.era === "modern" && isJSONRPCRequest(message) && message.method === "initialize" && !carriesValidModernEnvelopeClaim(message.params)) {
77690
+ await answerLegacyRejection(message, "initialize", message.params !== null && typeof message.params === "object" && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0);
77691
+ return;
77692
+ }
77693
+ if (state.era === "modern" && await tryServeListen(message)) return;
77694
+ state.instance.channel.deliver(message);
77695
+ return;
77696
+ }
77697
+ if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) {
77698
+ reportError(/* @__PURE__ */ new Error("Discarded a JSON-RPC response received before the connection negotiated an era"));
77699
+ return;
77700
+ }
77701
+ const opening = classifyOpeningMessage(message);
77702
+ switch (opening.kind) {
77703
+ case "invalid-envelope": {
77704
+ const detail = `Invalid _meta envelope for protocol revision 2026-07-28: ${opening.issue.key}: ${opening.issue.problem}`;
77705
+ if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InvalidParams, detail, { envelope: opening.issue });
77706
+ else reportError(/* @__PURE__ */ new Error(`Discarded a notification with a malformed envelope: ${detail}`));
77707
+ return;
77708
+ }
77709
+ case "unsupported-revision":
77710
+ if (isJSONRPCRequest(message)) {
77711
+ const error62 = new UnsupportedProtocolVersionError({
77712
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
77713
+ requested: opening.requested
77714
+ });
77715
+ reportError(error62);
77716
+ await writeErrorResponse(message.id, error62.code, error62.message, error62.data);
77717
+ } else reportError(/* @__PURE__ */ new Error(`Discarded a notification claiming unsupported protocol revision ${opening.requested}`));
77718
+ return;
77719
+ case "modern":
77720
+ if (isJSONRPCRequest(message) && message.method === "server/discover") {
77721
+ if (state.phase === "probe") {
77722
+ state.instance.channel.deliver(message, { classification: opening.classification });
77723
+ return;
77724
+ }
77725
+ const instance = await connectInstance("modern", opening.revision);
77726
+ if (isTornDown()) {
77727
+ await disposeLateInstance(instance);
77728
+ return;
77729
+ }
77730
+ state = {
77731
+ phase: "probe",
77732
+ instance
77733
+ };
77734
+ instance.channel.deliver(message, { classification: opening.classification });
77735
+ return;
77736
+ }
77737
+ if (state.phase === "probe") {
77738
+ if (isJSONRPCNotification(message)) {
77739
+ state.instance.channel.deliver(message, { classification: opening.classification });
77740
+ return;
77741
+ }
77742
+ state = {
77743
+ phase: "pinned",
77744
+ era: "modern",
77745
+ instance: state.instance
77746
+ };
77747
+ } else {
77748
+ const instance = await connectInstance("modern", opening.revision);
77749
+ if (isTornDown()) {
77750
+ await disposeLateInstance(instance);
77751
+ return;
77752
+ }
77753
+ state = {
77754
+ phase: "pinned",
77755
+ era: "modern",
77756
+ instance
77757
+ };
77758
+ }
77759
+ if (await tryServeListen(message)) return;
77760
+ state.instance.channel.deliver(message, { classification: opening.classification });
77761
+ return;
77762
+ case "legacy": {
77763
+ if (legacyMode === "reject") {
77764
+ if (isJSONRPCRequest(message)) await answerLegacyRejection(message, opening.reason, opening.requestedVersion);
77765
+ return;
77766
+ }
77767
+ if (state.phase === "probe") {
77768
+ await discardProbeInstance(state.instance);
77769
+ if (isTornDown()) return;
77770
+ state = { phase: "opening" };
77771
+ }
77772
+ const instance = await connectInstance("legacy");
77773
+ if (isTornDown()) {
77774
+ await disposeLateInstance(instance);
77775
+ return;
77776
+ }
77777
+ state = {
77778
+ phase: "pinned",
77779
+ era: "legacy",
77780
+ instance
77781
+ };
77782
+ state.instance.channel.deliver(message);
77783
+ return;
77784
+ }
77785
+ }
77786
+ };
77787
+ const queue = [];
77788
+ let pumping = false;
77789
+ const pump = async () => {
77790
+ if (pumping) return;
77791
+ pumping = true;
77792
+ try {
77793
+ while (queue.length > 0) {
77794
+ const message = queue.shift();
77795
+ try {
77796
+ await processMessage(message);
77797
+ } catch (error62) {
77798
+ if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InternalError, "Internal server error");
77799
+ reportError(toError(error62));
77800
+ }
77801
+ }
77802
+ } finally {
77803
+ pumping = false;
77804
+ }
77805
+ };
77806
+ const closeAll = async () => {
77807
+ if (closing || state.phase === "closed") return;
77808
+ closing = true;
77809
+ const current = state;
77810
+ state = { phase: "closed" };
77811
+ for (const result of listenRouter.teardownAll()) await wire.send(result).catch((error62) => reportError(toError(error62)));
77812
+ if (current.phase === "probe" || current.phase === "pinned") await current.instance.product.close().catch((error62) => reportError(toError(error62)));
77813
+ await wire.close().catch((error62) => reportError(toError(error62)));
77814
+ };
77815
+ wire.onmessage = (message) => {
77816
+ queue.push(message);
77817
+ pump();
77818
+ };
77819
+ wire.onerror = (error62) => {
77820
+ reportError(error62);
77821
+ if (state.phase === "probe" || state.phase === "pinned") state.instance.channel.onerror?.(error62);
77822
+ };
77823
+ wire.onclose = () => {
77824
+ if (closing || state.phase === "closed") return;
77825
+ closing = true;
77826
+ const current = state;
77827
+ state = { phase: "closed" };
77828
+ if (current.phase === "probe" || current.phase === "pinned") current.instance.product.close().catch((error62) => reportError(toError(error62)));
77829
+ };
77830
+ const started = wire.start().catch((error62) => {
77831
+ reportError(toError(error62));
77832
+ throw error62;
77833
+ });
77834
+ started.catch(() => {
77835
+ });
77836
+ return { close: async () => {
77837
+ await started.catch(() => {
77838
+ });
77839
+ await closeAll();
77840
+ } };
77841
+ }
77842
+ function toError(value) {
77843
+ return value instanceof Error ? value : new Error(String(value));
77844
+ }
77199
77845
 
77200
77846
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
77201
77847
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
@@ -97068,7 +97714,7 @@ async function createMcpServer(opts) {
97068
97714
  }
97069
97715
  return server;
97070
97716
  }
97071
- function withGracefulShutdown(server, opts = {}) {
97717
+ function withGracefulShutdown(target, opts = {}) {
97072
97718
  const shouldExit = opts.exit ?? true;
97073
97719
  let shuttingDown = false;
97074
97720
  const handler = (signal) => {
@@ -97079,7 +97725,7 @@ function withGracefulShutdown(server, opts = {}) {
97079
97725
  try {
97080
97726
  if (opts.onSignal)
97081
97727
  await opts.onSignal(signal);
97082
- await server.close();
97728
+ await target.close();
97083
97729
  } catch (err) {
97084
97730
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
97085
97731
  } finally {
@@ -97091,16 +97737,26 @@ function withGracefulShutdown(server, opts = {}) {
97091
97737
  process.on("SIGINT", () => handler("SIGINT"));
97092
97738
  process.on("SIGTERM", () => handler("SIGTERM"));
97093
97739
  }
97094
- async function runMcp(opts) {
97095
- const server = await createMcpServer(opts);
97740
+ function runMcp(opts) {
97741
+ if (opts.banner !== void 0) {
97742
+ console.error(opts.banner);
97743
+ }
97744
+ const spec = opts.transport ?? "stdio";
97745
+ const handle = serveStdio(() => createMcpServer({ ...opts, banner: void 0 }), {
97746
+ legacy: opts.legacy ?? "serve",
97747
+ onerror: opts.onerror ?? ((error62) => {
97748
+ console.error(`[mcp-utils] stdio server error: ${redactSecrets(error62.message)}`);
97749
+ }),
97750
+ // `'stdio'` means "let the entry make its own StdioServerTransport over
97751
+ // this process"; anything else is the caller's transport, which the entry
97752
+ // then owns (it starts it, pumps it, and closes it with the handle).
97753
+ ...spec === "stdio" ? {} : { transport: spec }
97754
+ });
97096
97755
  const shutdown = opts.shutdown ?? true;
97097
97756
  if (shutdown !== false) {
97098
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
97757
+ withGracefulShutdown(handle, shutdown === true ? {} : shutdown);
97099
97758
  }
97100
- const spec = opts.transport ?? "stdio";
97101
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
97102
- await server.connect(transport);
97103
- return server;
97759
+ return handle;
97104
97760
  }
97105
97761
 
97106
97762
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
@@ -98588,7 +99244,7 @@ function registerUtilityTools(server, client2) {
98588
99244
  // src/index.ts
98589
99245
  await runMcp({
98590
99246
  name: "splitwise-mcp",
98591
- version: "3.0.0",
99247
+ version: "3.1.0",
98592
99248
  // x-release-please-version
98593
99249
  deps: client,
98594
99250
  banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { registerUtilityTools } from './tools/utilities.js';
19
19
  // client into the same registrars instead of this singleton.
20
20
  await runMcp({
21
21
  name: 'splitwise-mcp',
22
- version: '3.0.0', // x-release-please-version
22
+ version: '3.1.0', // x-release-please-version
23
23
  deps: client,
24
24
  banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
25
25
  tools: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "mcpName": "io.github.chrischall/splitwise-mcp",
5
5
  "description": "Splitwise MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^0.28.0",
48
+ "@chrischall/mcp-utils": "^1.0.0",
49
49
  "@modelcontextprotocol/server": "^2.0.0",
50
50
  "dotenv": "^17.4.0",
51
51
  "unpdf": "^1.8.1",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/splitwise-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.0.0",
9
+ "version": "3.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "splitwise-mcp",
14
- "version": "3.0.0",
14
+ "version": "3.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },