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.
@@ -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.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.1",
18
+ "version": "3.1.2",
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.1.1",
4
+ "version": "3.1.2",
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
@@ -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 profile fields |
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
- return this.api.fetchJson(method, path, body !== void 0 ? { body } : {});
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, email: email4, password, locale, default_currency, confirm }) => {
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
- previewBody
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.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
- return this.api.fetchJson(method, path, body !== undefined ? { body } : {});
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.1', // x-release-please-version
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: [
@@ -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, email, password, locale, default_currency, confirm }) => {
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
- // Never echo the password in the dry-run preview.
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.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.1",
9
+ "version": "3.1.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "splitwise-mcp",
14
- "version": "3.1.1",
14
+ "version": "3.1.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },