splitwise-mcp 2.2.2 → 2.3.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/dist/bundle.js +148 -2
- package/dist/client.js +20 -1
- package/dist/index.js +3 -1
- package/dist/tools/healthcheck.js +28 -0
- package/mint.yaml +11 -2
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -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": "2.
|
|
10
|
+
"version": "2.3.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": "2.
|
|
18
|
+
"version": "2.3.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -124,6 +124,7 @@ Add to Claude Desktop config:
|
|
|
124
124
|
| Tool | What it does |
|
|
125
125
|
|------|-------------|
|
|
126
126
|
| `sw_get_current_user` | Get the authenticated user's profile |
|
|
127
|
+
| `sw_healthcheck` | Verify the API key and Splitwise reachability; says which of the two failed |
|
|
127
128
|
| `sw_get_user` | Get another user's profile by ID |
|
|
128
129
|
| `sw_update_user` | Update the current user's profile fields |
|
|
129
130
|
|
|
@@ -202,6 +203,7 @@ src/
|
|
|
202
203
|
index.ts MCP server entry point
|
|
203
204
|
tools/
|
|
204
205
|
user.ts sw_get_current_user, sw_get_user
|
|
206
|
+
healthcheck.ts sw_healthcheck
|
|
205
207
|
groups.ts group CRUD and membership
|
|
206
208
|
friends.ts friend list and management
|
|
207
209
|
expenses.ts expense CRUD
|
package/dist/bundle.js
CHANGED
|
@@ -70944,6 +70944,11 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
|
70944
70944
|
return redacted;
|
|
70945
70945
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
70946
70946
|
}
|
|
70947
|
+
function messageOf(err) {
|
|
70948
|
+
if (err instanceof Error)
|
|
70949
|
+
return err.message;
|
|
70950
|
+
return String(err);
|
|
70951
|
+
}
|
|
70947
70952
|
|
|
70948
70953
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
70949
70954
|
function textResult(data) {
|
|
@@ -71380,6 +71385,8 @@ function redactAssetQuery(err, search) {
|
|
|
71380
71385
|
}
|
|
71381
71386
|
var SplitwiseClient = class {
|
|
71382
71387
|
apiKey;
|
|
71388
|
+
/** Which source supplied the API key — a LABEL, never the value. */
|
|
71389
|
+
credentialSource;
|
|
71383
71390
|
configError;
|
|
71384
71391
|
api;
|
|
71385
71392
|
/** Per-origin clients for asset hosts, built on first use by `fetchAsset`. */
|
|
@@ -71395,7 +71402,9 @@ var SplitwiseClient = class {
|
|
|
71395
71402
|
* byte-for-byte identical behaviour.
|
|
71396
71403
|
*/
|
|
71397
71404
|
constructor(opts) {
|
|
71398
|
-
const
|
|
71405
|
+
const injected = opts?.apiKey;
|
|
71406
|
+
const key = injected ?? readEnvVar("SPLITWISE_API_KEY");
|
|
71407
|
+
this.credentialSource = !key ? null : injected !== void 0 ? "injected" : "env";
|
|
71399
71408
|
if (!key) {
|
|
71400
71409
|
this.apiKey = null;
|
|
71401
71410
|
this.configError = new Error("SPLITWISE_API_KEY environment variable is required");
|
|
@@ -71405,6 +71414,14 @@ var SplitwiseClient = class {
|
|
|
71405
71414
|
}
|
|
71406
71415
|
this.api = this.buildApiClient(BASE_URL, true);
|
|
71407
71416
|
}
|
|
71417
|
+
/**
|
|
71418
|
+
* Which source supplied the API key, for `sw_healthcheck`. Returns a LABEL
|
|
71419
|
+
* or `null` — deliberately never the key, since the healthcheck result is
|
|
71420
|
+
* the thing people paste into a chat when something is broken.
|
|
71421
|
+
*/
|
|
71422
|
+
describeCredential() {
|
|
71423
|
+
return { source: this.credentialSource };
|
|
71424
|
+
}
|
|
71408
71425
|
/**
|
|
71409
71426
|
* One API client per origin, sharing the fleet's retry/timeout/error policy.
|
|
71410
71427
|
* `authenticated` decides whether the API key is attached: `getToken` defers
|
|
@@ -71514,6 +71531,134 @@ function registerUserTools(server, client2) {
|
|
|
71514
71531
|
});
|
|
71515
71532
|
}
|
|
71516
71533
|
|
|
71534
|
+
// node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
|
|
71535
|
+
function statusOf(err) {
|
|
71536
|
+
if (typeof err !== "object" || err === null)
|
|
71537
|
+
return void 0;
|
|
71538
|
+
const s2 = err.status ?? err.statusCode;
|
|
71539
|
+
return typeof s2 === "number" ? s2 : void 0;
|
|
71540
|
+
}
|
|
71541
|
+
function credentialHint(arm, prefix, hostLabel, source) {
|
|
71542
|
+
switch (arm) {
|
|
71543
|
+
case "ok":
|
|
71544
|
+
return `Credential from '${source}' works: ${hostLabel} accepted an authenticated request. If a real tool still fails, the problem is that tool, not auth.`;
|
|
71545
|
+
case "no_credential":
|
|
71546
|
+
return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
|
|
71547
|
+
case "credential_rejected":
|
|
71548
|
+
return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
|
|
71549
|
+
case "timeout":
|
|
71550
|
+
return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
71551
|
+
case "http":
|
|
71552
|
+
return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
|
|
71553
|
+
case "transport":
|
|
71554
|
+
return `Could not reach ${hostLabel} at all. Check network egress; the credential itself was never judged.`;
|
|
71555
|
+
default:
|
|
71556
|
+
return `Unexpected failure \u2014 see error.message.`;
|
|
71557
|
+
}
|
|
71558
|
+
}
|
|
71559
|
+
function registerCredentialHealthcheckTool(args) {
|
|
71560
|
+
const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
|
|
71561
|
+
const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
|
|
71562
|
+
server.registerTool(`${prefix}_healthcheck`, {
|
|
71563
|
+
title: "Verify credentials and upstream reachability",
|
|
71564
|
+
description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
|
|
71565
|
+
annotations: {
|
|
71566
|
+
title: "Verify credentials and upstream reachability",
|
|
71567
|
+
readOnlyHint: true,
|
|
71568
|
+
idempotentHint: true,
|
|
71569
|
+
openWorldHint: true
|
|
71570
|
+
},
|
|
71571
|
+
inputSchema: {}
|
|
71572
|
+
}, async () => {
|
|
71573
|
+
let probeStarted = 0;
|
|
71574
|
+
let state;
|
|
71575
|
+
try {
|
|
71576
|
+
state = await resolveCredential();
|
|
71577
|
+
} catch (e2) {
|
|
71578
|
+
const result2 = {
|
|
71579
|
+
ok: false,
|
|
71580
|
+
credential: { source: null, resolved: false },
|
|
71581
|
+
// No `url`: nothing was probed, and naming one implies it was tried.
|
|
71582
|
+
probe: { elapsed_ms: 0 },
|
|
71583
|
+
error: { kind: "no_credential", message: truncateErrorMessage(messageOf(e2)) },
|
|
71584
|
+
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
71585
|
+
};
|
|
71586
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
71587
|
+
}
|
|
71588
|
+
const credential = {
|
|
71589
|
+
source: state.source,
|
|
71590
|
+
resolved: state.source !== null,
|
|
71591
|
+
...state.detail !== void 0 ? { detail: state.detail } : {}
|
|
71592
|
+
};
|
|
71593
|
+
if (!credential.resolved) {
|
|
71594
|
+
const result2 = {
|
|
71595
|
+
ok: false,
|
|
71596
|
+
credential,
|
|
71597
|
+
probe: { elapsed_ms: 0 },
|
|
71598
|
+
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
71599
|
+
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
71600
|
+
};
|
|
71601
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
71602
|
+
}
|
|
71603
|
+
let arm = "ok";
|
|
71604
|
+
let error51;
|
|
71605
|
+
let status;
|
|
71606
|
+
let customHint;
|
|
71607
|
+
probeStarted = Date.now();
|
|
71608
|
+
try {
|
|
71609
|
+
await probeFn();
|
|
71610
|
+
} catch (e2) {
|
|
71611
|
+
status = statusOf(e2);
|
|
71612
|
+
const aborted2 = e2 instanceof Error && e2.name === "AbortError";
|
|
71613
|
+
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e2)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e2)) ? "transport" : "unknown";
|
|
71614
|
+
let kind = arm;
|
|
71615
|
+
let detail;
|
|
71616
|
+
const custom2 = classifyThrown?.(e2);
|
|
71617
|
+
if (custom2) {
|
|
71618
|
+
kind = custom2.kind;
|
|
71619
|
+
customHint = custom2.hint;
|
|
71620
|
+
detail = custom2.detail;
|
|
71621
|
+
}
|
|
71622
|
+
error51 = {
|
|
71623
|
+
kind,
|
|
71624
|
+
// Redacted AND bounded before it reaches the result: an upstream
|
|
71625
|
+
// failure routinely quotes what it was sent, and a healthcheck is
|
|
71626
|
+
// the tool people paste into a chat when something is broken.
|
|
71627
|
+
message: truncateErrorMessage(messageOf(e2)),
|
|
71628
|
+
...detail !== void 0 ? { detail } : {}
|
|
71629
|
+
};
|
|
71630
|
+
}
|
|
71631
|
+
const result = {
|
|
71632
|
+
ok: error51 === void 0,
|
|
71633
|
+
credential,
|
|
71634
|
+
probe: {
|
|
71635
|
+
...probeUrl ? { url: probeUrl } : {},
|
|
71636
|
+
elapsed_ms: Date.now() - probeStarted,
|
|
71637
|
+
...status !== void 0 ? { status } : {}
|
|
71638
|
+
},
|
|
71639
|
+
...error51 ? { error: error51 } : {},
|
|
71640
|
+
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
71641
|
+
};
|
|
71642
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
71643
|
+
});
|
|
71644
|
+
}
|
|
71645
|
+
|
|
71646
|
+
// src/tools/healthcheck.ts
|
|
71647
|
+
function registerHealthcheckTools(server, client2) {
|
|
71648
|
+
registerCredentialHealthcheckTool({
|
|
71649
|
+
server,
|
|
71650
|
+
prefix: "sw",
|
|
71651
|
+
hostLabel: "secure.splitwise.com",
|
|
71652
|
+
probePath: "/api/v3.0/get_current_user",
|
|
71653
|
+
resolveCredential: async () => client2.describeCredential(),
|
|
71654
|
+
probeFn: () => client2.request("GET", "/get_current_user"),
|
|
71655
|
+
hints: {
|
|
71656
|
+
no_credential: "No Splitwise API key resolved. Set SPLITWISE_API_KEY, or reconnect the connector so it supplies one.",
|
|
71657
|
+
credential_rejected: "Splitwise rejected the API key. Generate a new one at https://secure.splitwise.com/apps and update SPLITWISE_API_KEY (or reconnect the connector)."
|
|
71658
|
+
}
|
|
71659
|
+
});
|
|
71660
|
+
}
|
|
71661
|
+
|
|
71517
71662
|
// src/tools/groups.ts
|
|
71518
71663
|
function registerGroupTools(server, client2) {
|
|
71519
71664
|
server.registerTool("sw_list_groups", {
|
|
@@ -72010,12 +72155,13 @@ function registerUtilityTools(server, client2) {
|
|
|
72010
72155
|
// src/index.ts
|
|
72011
72156
|
await runMcp({
|
|
72012
72157
|
name: "splitwise-mcp",
|
|
72013
|
-
version: "2.
|
|
72158
|
+
version: "2.3.0",
|
|
72014
72159
|
// x-release-please-version
|
|
72015
72160
|
deps: client,
|
|
72016
72161
|
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
|
72017
72162
|
tools: [
|
|
72018
72163
|
registerUserTools,
|
|
72164
|
+
registerHealthcheckTools,
|
|
72019
72165
|
registerGroupTools,
|
|
72020
72166
|
registerFriendTools,
|
|
72021
72167
|
registerExpenseTools,
|
package/dist/client.js
CHANGED
|
@@ -61,6 +61,8 @@ function redactAssetQuery(err, search) {
|
|
|
61
61
|
}
|
|
62
62
|
export class SplitwiseClient {
|
|
63
63
|
apiKey;
|
|
64
|
+
/** Which source supplied the API key — a LABEL, never the value. */
|
|
65
|
+
credentialSource;
|
|
64
66
|
configError;
|
|
65
67
|
api;
|
|
66
68
|
/** Per-origin clients for asset hosts, built on first use by `fetchAsset`. */
|
|
@@ -80,7 +82,16 @@ export class SplitwiseClient {
|
|
|
80
82
|
// back to the env var. readEnvVar trims whitespace and treats
|
|
81
83
|
// blank/`undefined`/`null`/`${...}` placeholder values as unset — defends
|
|
82
84
|
// against MCP hosts that pass .mcp.json env blocks through unexpanded.
|
|
83
|
-
const
|
|
85
|
+
const injected = opts?.apiKey;
|
|
86
|
+
const key = injected ?? readEnvVar('SPLITWISE_API_KEY');
|
|
87
|
+
// Which source supplied it, for `sw_healthcheck`. Derived from the SAME
|
|
88
|
+
// `key` and `injected` bindings the resolution above uses, so the two
|
|
89
|
+
// cannot disagree. An earlier version re-tested `opts?.apiKey` for
|
|
90
|
+
// TRUTHINESS while the resolution used `??`: an injected empty string is
|
|
91
|
+
// not nullish, so `key` became `''` and the client reported a config error
|
|
92
|
+
// while the source said `env` — the healthcheck naming a source the client
|
|
93
|
+
// had not used.
|
|
94
|
+
this.credentialSource = !key ? null : injected !== undefined ? 'injected' : 'env';
|
|
84
95
|
if (!key) {
|
|
85
96
|
this.apiKey = null;
|
|
86
97
|
this.configError = new Error('SPLITWISE_API_KEY environment variable is required');
|
|
@@ -91,6 +102,14 @@ export class SplitwiseClient {
|
|
|
91
102
|
}
|
|
92
103
|
this.api = this.buildApiClient(BASE_URL, true);
|
|
93
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Which source supplied the API key, for `sw_healthcheck`. Returns a LABEL
|
|
107
|
+
* or `null` — deliberately never the key, since the healthcheck result is
|
|
108
|
+
* the thing people paste into a chat when something is broken.
|
|
109
|
+
*/
|
|
110
|
+
describeCredential() {
|
|
111
|
+
return { source: this.credentialSource };
|
|
112
|
+
}
|
|
94
113
|
/**
|
|
95
114
|
* One API client per origin, sharing the fleet's retry/timeout/error policy.
|
|
96
115
|
* `authenticated` decides whether the API key is attached: `getToken` defers
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { runMcp } from '@chrischall/mcp-utils';
|
|
3
3
|
import { client } from './client.js';
|
|
4
4
|
import { registerUserTools } from './tools/user.js';
|
|
5
|
+
import { registerHealthcheckTools } from './tools/healthcheck.js';
|
|
5
6
|
import { registerGroupTools } from './tools/groups.js';
|
|
6
7
|
import { registerFriendTools } from './tools/friends.js';
|
|
7
8
|
import { registerExpenseTools } from './tools/expenses.js';
|
|
@@ -18,11 +19,12 @@ import { registerUtilityTools } from './tools/utilities.js';
|
|
|
18
19
|
// client into the same registrars instead of this singleton.
|
|
19
20
|
await runMcp({
|
|
20
21
|
name: 'splitwise-mcp',
|
|
21
|
-
version: '2.
|
|
22
|
+
version: '2.3.0', // x-release-please-version
|
|
22
23
|
deps: client,
|
|
23
24
|
banner: '[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.',
|
|
24
25
|
tools: [
|
|
25
26
|
registerUserTools,
|
|
27
|
+
registerHealthcheckTools,
|
|
26
28
|
registerGroupTools,
|
|
27
29
|
registerFriendTools,
|
|
28
30
|
registerExpenseTools,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { registerCredentialHealthcheckTool } from '@chrischall/mcp-utils/healthcheck';
|
|
2
|
+
/**
|
|
3
|
+
* Register `sw_healthcheck` — resolves the API key the way real tools do, then
|
|
4
|
+
* makes one authenticated call to `/get_current_user`.
|
|
5
|
+
*
|
|
6
|
+
* Splitwise has no browser bridge, so "health" here is entirely about the
|
|
7
|
+
* credential: whether one resolved at all, and whether Splitwise still accepts
|
|
8
|
+
* it. Those are different problems with different fixes and, without this,
|
|
9
|
+
* both surface as the same opaque tool error.
|
|
10
|
+
*
|
|
11
|
+
* `/get_current_user` is the probe because it is the cheapest endpoint that
|
|
12
|
+
* REQUIRES auth — an unauthenticated endpoint would pass while the key was
|
|
13
|
+
* expired, which is the failure most worth catching.
|
|
14
|
+
*/
|
|
15
|
+
export function registerHealthcheckTools(server, client) {
|
|
16
|
+
registerCredentialHealthcheckTool({
|
|
17
|
+
server,
|
|
18
|
+
prefix: 'sw',
|
|
19
|
+
hostLabel: 'secure.splitwise.com',
|
|
20
|
+
probePath: '/api/v3.0/get_current_user',
|
|
21
|
+
resolveCredential: async () => client.describeCredential(),
|
|
22
|
+
probeFn: () => client.request('GET', '/get_current_user'),
|
|
23
|
+
hints: {
|
|
24
|
+
no_credential: 'No Splitwise API key resolved. Set SPLITWISE_API_KEY, or reconnect the connector so it supplies one.',
|
|
25
|
+
credential_rejected: 'Splitwise rejected the API key. Generate a new one at https://secure.splitwise.com/apps and update SPLITWISE_API_KEY (or reconnect the connector).',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
package/mint.yaml
CHANGED
|
@@ -13,6 +13,15 @@ env:
|
|
|
13
13
|
https://secure.splitwise.com/apps/register and copy its API key.
|
|
14
14
|
egress:
|
|
15
15
|
allow:
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# Only hosts the SERVER process actually fetches. Hosts that appear
|
|
17
|
+
# solely in a URL this server BUILDS and returns are excluded.
|
|
18
|
+
#
|
|
19
|
+
# The Splitwise REST API (https://secure.splitwise.com/api/v3.0) — every
|
|
20
|
+
# tool but sw_get_receipt talks only to this host.
|
|
18
21
|
- secure.splitwise.com
|
|
22
|
+
# sw_get_receipt fetches the receipt URL the API hands back, which lives at
|
|
23
|
+
# https://www.splitwise.com/api/v4.0/expenses/{id}/receipt (checked live
|
|
24
|
+
# 2026-08-28) and 302s to the presigned S3 object below. The child's fetch
|
|
25
|
+
# follows that redirect, so each hop is its own CONNECT and must be listed.
|
|
26
|
+
- www.splitwise.com
|
|
27
|
+
- splitwise.s3.amazonaws.com
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splitwise-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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.
|
|
48
|
+
"@chrischall/mcp-utils": "^0.19.2",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.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": "2.
|
|
9
|
+
"version": "2.3.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "splitwise-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.3.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|