splitwise-mcp 3.1.1 → 3.1.2
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 +1 -1
- package/dist/bundle.js +33 -10
- package/dist/client.js +35 -1
- package/dist/index.js +1 -1
- package/dist/tools/user.js +10 -9
- package/package.json +1 -1
- 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": "3.1.
|
|
10
|
+
"version": "3.1.2"
|
|
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.1.
|
|
18
|
+
"version": "3.1.2",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ Add to Claude Desktop config:
|
|
|
126
126
|
| `sw_get_current_user` | Get the authenticated user's profile |
|
|
127
127
|
| `sw_healthcheck` | Verify the API key and Splitwise reachability; says which of the two failed |
|
|
128
128
|
| `sw_get_user` | Get another user's profile by ID |
|
|
129
|
-
| `sw_update_user` | Update the current user's
|
|
129
|
+
| `sw_update_user` | Update the current user's name, locale or default currency (login email and password are not changeable here) |
|
|
130
130
|
|
|
131
131
|
### Groups
|
|
132
132
|
|
package/dist/bundle.js
CHANGED
|
@@ -98340,6 +98340,25 @@ function redactAssetQuery(err, search) {
|
|
|
98340
98340
|
if (message === err.message) return err;
|
|
98341
98341
|
return err instanceof ApiError ? new ApiError(err.status, message) : new Error(message);
|
|
98342
98342
|
}
|
|
98343
|
+
function flattenErrors(errors) {
|
|
98344
|
+
if (errors == null) return [];
|
|
98345
|
+
if (typeof errors === "string") return errors ? [errors] : [];
|
|
98346
|
+
if (Array.isArray(errors)) return errors.flatMap((e2) => flattenErrors(e2));
|
|
98347
|
+
if (typeof errors === "object") {
|
|
98348
|
+
return Object.entries(errors).flatMap(
|
|
98349
|
+
([field, value]) => flattenErrors(value).map((msg) => field === "base" ? msg : `${field}: ${msg}`)
|
|
98350
|
+
);
|
|
98351
|
+
}
|
|
98352
|
+
return [String(errors)];
|
|
98353
|
+
}
|
|
98354
|
+
function assertWriteSucceeded(data) {
|
|
98355
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return;
|
|
98356
|
+
const record4 = data;
|
|
98357
|
+
const messages = flattenErrors(record4.errors);
|
|
98358
|
+
if (messages.length === 0 && record4.success !== false) return;
|
|
98359
|
+
const detail = messages.length > 0 ? messages.join("; ") : "success: false with no error detail";
|
|
98360
|
+
throw new Error(`Splitwise rejected the request: ${detail}`);
|
|
98361
|
+
}
|
|
98343
98362
|
var SplitwiseClient = class {
|
|
98344
98363
|
apiKey;
|
|
98345
98364
|
/** Which source supplied the API key — a LABEL, never the value. */
|
|
@@ -98404,7 +98423,9 @@ var SplitwiseClient = class {
|
|
|
98404
98423
|
return this.apiKey;
|
|
98405
98424
|
}
|
|
98406
98425
|
async request(method, path, body) {
|
|
98407
|
-
|
|
98426
|
+
const data = await this.api.fetchJson(method, path, body !== void 0 ? { body } : {});
|
|
98427
|
+
if (method.toUpperCase() !== "GET") assertWriteSucceeded(data);
|
|
98428
|
+
return data;
|
|
98408
98429
|
}
|
|
98409
98430
|
/**
|
|
98410
98431
|
* Fetch the raw bytes behind an absolute asset URL the API handed back (an
|
|
@@ -98588,34 +98609,36 @@ function registerUserTools(server, client2) {
|
|
|
98588
98609
|
server.registerTool(
|
|
98589
98610
|
"sw_update_user",
|
|
98590
98611
|
{
|
|
98591
|
-
description: "Update the current user's profile fields. id must be the current user's id.",
|
|
98612
|
+
description: "Update the current user's profile fields: name, locale and default currency. id must be the current user's id. The login email and password are deliberately not settable here \u2014 account credentials are changed in the Splitwise app, not by an assistant.",
|
|
98613
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
98614
|
+
// No `email` / `password`: changing the login email is an account
|
|
98615
|
+
// takeover primitive (a password reset to the new address follows), and
|
|
98616
|
+
// the only gate would be a model-set `confirm` that injected text from
|
|
98617
|
+
// comments, notifications or receipts could talk it into (fleet-audit
|
|
98618
|
+
// #250). z.object strips unknown keys, so a caller passing them anyway
|
|
98619
|
+
// has them dropped before the request is built.
|
|
98592
98620
|
inputSchema: external_exports.object({
|
|
98593
98621
|
id: external_exports.number().describe("User ID (must be the current user's id)"),
|
|
98594
98622
|
first_name: external_exports.string().optional(),
|
|
98595
98623
|
last_name: external_exports.string().optional(),
|
|
98596
|
-
email: external_exports.string().optional(),
|
|
98597
|
-
password: external_exports.string().optional(),
|
|
98598
98624
|
locale: external_exports.string().optional(),
|
|
98599
98625
|
default_currency: external_exports.string().optional(),
|
|
98600
98626
|
confirm: schemaConfirm
|
|
98601
98627
|
})
|
|
98602
98628
|
},
|
|
98603
|
-
async ({ id, first_name, last_name,
|
|
98629
|
+
async ({ id, first_name, last_name, locale, default_currency, confirm }) => {
|
|
98604
98630
|
const body = pruneUndefined({
|
|
98605
98631
|
first_name,
|
|
98606
98632
|
last_name,
|
|
98607
|
-
email: email4,
|
|
98608
|
-
password,
|
|
98609
98633
|
locale,
|
|
98610
98634
|
default_currency
|
|
98611
98635
|
});
|
|
98612
|
-
const previewBody = { ...body, ...password !== void 0 ? { password: "[hidden]" } : {} };
|
|
98613
98636
|
const gate = previewUnlessConfirmed(
|
|
98614
98637
|
confirm,
|
|
98615
98638
|
`Update current Splitwise user ${id} profile`,
|
|
98616
98639
|
"POST",
|
|
98617
98640
|
`/update_user/${id}`,
|
|
98618
|
-
|
|
98641
|
+
body
|
|
98619
98642
|
);
|
|
98620
98643
|
if (gate) return gate;
|
|
98621
98644
|
const data = await client2.request("POST", `/update_user/${id}`, body);
|
|
@@ -99487,7 +99510,7 @@ function registerUtilityTools(server, client2) {
|
|
|
99487
99510
|
// src/index.ts
|
|
99488
99511
|
await runMcp({
|
|
99489
99512
|
name: "splitwise-mcp",
|
|
99490
|
-
version: "3.1.
|
|
99513
|
+
version: "3.1.2",
|
|
99491
99514
|
// x-release-please-version
|
|
99492
99515
|
deps: client,
|
|
99493
99516
|
banner: "[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.",
|
package/dist/client.js
CHANGED
|
@@ -59,6 +59,37 @@ function redactAssetQuery(err, search) {
|
|
|
59
59
|
return err;
|
|
60
60
|
return err instanceof ApiError ? new ApiError(err.status, message) : new Error(message);
|
|
61
61
|
}
|
|
62
|
+
/** Flatten Splitwise's `errors` value (`{field: [msg]}`, `[msg]` or a string) into messages. */
|
|
63
|
+
function flattenErrors(errors) {
|
|
64
|
+
if (errors == null)
|
|
65
|
+
return [];
|
|
66
|
+
if (typeof errors === 'string')
|
|
67
|
+
return errors ? [errors] : [];
|
|
68
|
+
if (Array.isArray(errors))
|
|
69
|
+
return errors.flatMap((e) => flattenErrors(e));
|
|
70
|
+
if (typeof errors === 'object') {
|
|
71
|
+
return Object.entries(errors).flatMap(([field, value]) => flattenErrors(value).map((msg) => (field === 'base' ? msg : `${field}: ${msg}`)));
|
|
72
|
+
}
|
|
73
|
+
return [String(errors)];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Splitwise answers a failed write with HTTP 200, not an error status:
|
|
77
|
+
* create/update return `{expenses: [], errors: {...}}` and delete/undelete
|
|
78
|
+
* return `{success: false, errors: {...}}`. `fetchJson` only throws on
|
|
79
|
+
* non-2xx, so without this check a rejected write reaches the tool as an
|
|
80
|
+
* ordinary result and reads as "created"/"deleted". Throwing here makes every
|
|
81
|
+
* write tool report it with `isError`.
|
|
82
|
+
*/
|
|
83
|
+
function assertWriteSucceeded(data) {
|
|
84
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data))
|
|
85
|
+
return;
|
|
86
|
+
const record = data;
|
|
87
|
+
const messages = flattenErrors(record.errors);
|
|
88
|
+
if (messages.length === 0 && record.success !== false)
|
|
89
|
+
return;
|
|
90
|
+
const detail = messages.length > 0 ? messages.join('; ') : 'success: false with no error detail';
|
|
91
|
+
throw new Error(`Splitwise rejected the request: ${detail}`);
|
|
92
|
+
}
|
|
62
93
|
export class SplitwiseClient {
|
|
63
94
|
apiKey;
|
|
64
95
|
/** Which source supplied the API key — a LABEL, never the value. */
|
|
@@ -138,7 +169,10 @@ export class SplitwiseClient {
|
|
|
138
169
|
return this.apiKey;
|
|
139
170
|
}
|
|
140
171
|
async request(method, path, body) {
|
|
141
|
-
|
|
172
|
+
const data = await this.api.fetchJson(method, path, body !== undefined ? { body } : {});
|
|
173
|
+
if (method.toUpperCase() !== 'GET')
|
|
174
|
+
assertWriteSucceeded(data);
|
|
175
|
+
return data;
|
|
142
176
|
}
|
|
143
177
|
/**
|
|
144
178
|
* Fetch the raw bytes behind an absolute asset URL the API handed back (an
|
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.1.
|
|
22
|
+
version: '3.1.2', // 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/dist/tools/user.js
CHANGED
|
@@ -25,29 +25,30 @@ export function registerUserTools(server, client) {
|
|
|
25
25
|
return minifiedResult(viewUser(resolveView(view, SW_VIEWS), data));
|
|
26
26
|
});
|
|
27
27
|
server.registerTool('sw_update_user', {
|
|
28
|
-
description: "Update the current user's profile fields. id must be the current user's id.",
|
|
28
|
+
description: "Update the current user's profile fields: name, locale and default currency. id must be the current user's id. The login email and password are deliberately not settable here — account credentials are changed in the Splitwise app, not by an assistant.",
|
|
29
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
30
|
+
// No `email` / `password`: changing the login email is an account
|
|
31
|
+
// takeover primitive (a password reset to the new address follows), and
|
|
32
|
+
// the only gate would be a model-set `confirm` that injected text from
|
|
33
|
+
// comments, notifications or receipts could talk it into (fleet-audit
|
|
34
|
+
// #250). z.object strips unknown keys, so a caller passing them anyway
|
|
35
|
+
// has them dropped before the request is built.
|
|
29
36
|
inputSchema: z.object({
|
|
30
37
|
id: z.number().describe("User ID (must be the current user's id)"),
|
|
31
38
|
first_name: z.string().optional(),
|
|
32
39
|
last_name: z.string().optional(),
|
|
33
|
-
email: z.string().optional(),
|
|
34
|
-
password: z.string().optional(),
|
|
35
40
|
locale: z.string().optional(),
|
|
36
41
|
default_currency: z.string().optional(),
|
|
37
42
|
confirm: schemaConfirm,
|
|
38
43
|
}),
|
|
39
|
-
}, async ({ id, first_name, last_name,
|
|
44
|
+
}, async ({ id, first_name, last_name, locale, default_currency, confirm }) => {
|
|
40
45
|
const body = pruneUndefined({
|
|
41
46
|
first_name,
|
|
42
47
|
last_name,
|
|
43
|
-
email,
|
|
44
|
-
password,
|
|
45
48
|
locale,
|
|
46
49
|
default_currency,
|
|
47
50
|
});
|
|
48
|
-
|
|
49
|
-
const previewBody = { ...body, ...(password !== undefined ? { password: '[hidden]' } : {}) };
|
|
50
|
-
const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, 'POST', `/update_user/${id}`, previewBody);
|
|
51
|
+
const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, 'POST', `/update_user/${id}`, body);
|
|
51
52
|
if (gate)
|
|
52
53
|
return gate;
|
|
53
54
|
const data = await client.request('POST', `/update_user/${id}`, body);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splitwise-mcp",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
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>",
|
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.1.
|
|
9
|
+
"version": "3.1.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "splitwise-mcp",
|
|
14
|
-
"version": "3.1.
|
|
14
|
+
"version": "3.1.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|