splitwise-mcp 2.4.2 → 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.
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: '2.4.2', // 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: [
@@ -36,17 +36,25 @@ export function registerExpenseTools(server, client) {
36
36
  server.registerTool('sw_list_expenses', {
37
37
  description: 'List or search Splitwise expenses. All filters are optional. Use group_id to filter by group, dated_after/dated_before for date ranges.',
38
38
  annotations: { readOnlyHint: true },
39
- inputSchema: {
40
- view: viewParam(SW_VIEWS, { note: 'compact keeps the share breakdown, repayments and receipt presence and drops the avatars and the repeat/reminder/transaction block; "full" returns Splitwise\'s whole records.' }),
39
+ inputSchema: z.object({
40
+ view: viewParam(SW_VIEWS, {
41
+ note: 'compact keeps the share breakdown, repayments and receipt presence and drops the avatars and the repeat/reminder/transaction block; "full" returns Splitwise\'s whole records.',
42
+ }),
41
43
  group_id: z.number().describe('Only expenses in this group').optional(),
42
44
  friend_id: z.number().describe('Only expenses with this friend').optional(),
43
- dated_after: z.string().describe('ISO 8601 date — only expenses on or after this date').optional(),
44
- dated_before: z.string().describe('ISO 8601 date — only expenses on or before this date').optional(),
45
+ dated_after: z
46
+ .string()
47
+ .describe('ISO 8601 date — only expenses on or after this date')
48
+ .optional(),
49
+ dated_before: z
50
+ .string()
51
+ .describe('ISO 8601 date — only expenses on or before this date')
52
+ .optional(),
45
53
  updated_after: z.string().describe('ISO 8601 datetime').optional(),
46
54
  updated_before: z.string().describe('ISO 8601 datetime').optional(),
47
55
  limit: z.number().describe('Max results (API default: 20)').optional(),
48
56
  offset: z.number().describe('Pagination offset').optional(),
49
- },
57
+ }),
50
58
  }, async (args) => {
51
59
  const qs = buildQueryString({
52
60
  group_id: args.group_id,
@@ -64,28 +72,39 @@ export function registerExpenseTools(server, client) {
64
72
  server.registerTool('sw_get_expense', {
65
73
  description: 'Get full details of a single Splitwise expense by id.',
66
74
  annotations: { readOnlyHint: true },
67
- inputSchema: {
68
- view: viewParam(SW_VIEWS, { note: 'compact keeps the share breakdown, repayments and receipt presence and drops the avatars and the repeat/reminder/transaction block; "full" returns Splitwise\'s whole records.' }),
75
+ inputSchema: z.object({
76
+ view: viewParam(SW_VIEWS, {
77
+ note: 'compact keeps the share breakdown, repayments and receipt presence and drops the avatars and the repeat/reminder/transaction block; "full" returns Splitwise\'s whole records.',
78
+ }),
69
79
  id: z.number().describe('Expense ID'),
70
- },
80
+ }),
71
81
  }, async ({ id, view }) => {
72
82
  const data = await client.request('GET', `/get_expense/${id}`);
73
83
  return minifiedResult(viewExpense(resolveView(view, SW_VIEWS), data));
74
84
  });
75
85
  server.registerTool('sw_create_expense', {
76
86
  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. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it creates the expense.',
77
- inputSchema: {
87
+ inputSchema: z.object({
78
88
  group_id: z.number().describe('Group to add expense to (use 0 for no group)'),
79
89
  description: z.string().describe('Short description of the expense'),
80
90
  cost: z.string().describe('Total cost as decimal string, e.g. "25.00"'),
81
- split_equally: z.boolean().describe('Split equally among group members (mutually exclusive with users)').optional(),
82
- users: z.array(userShareSchema).describe('Custom split (mutually exclusive with split_equally). Full list of participants required.').optional(),
83
- currency_code: z.string().describe('Currency code, e.g. "USD". Defaults to group/user default.').optional(),
91
+ split_equally: z
92
+ .boolean()
93
+ .describe('Split equally among group members (mutually exclusive with users)')
94
+ .optional(),
95
+ users: z
96
+ .array(userShareSchema)
97
+ .describe('Custom split (mutually exclusive with split_equally). Full list of participants required.')
98
+ .optional(),
99
+ currency_code: z
100
+ .string()
101
+ .describe('Currency code, e.g. "USD". Defaults to group/user default.')
102
+ .optional(),
84
103
  date: z.string().describe('ISO 8601 datetime').optional(),
85
104
  category_id: z.number().describe('Category id from sw_get_categories').optional(),
86
105
  details: z.string().describe('Notes').optional(),
87
106
  confirm: schemaConfirm,
88
- },
107
+ }),
89
108
  }, async (args) => {
90
109
  const body = buildExpenseBody(args);
91
110
  const gate = previewUnlessConfirmed(args.confirm, `Create a Splitwise expense "${args.description}" (${args.cost}) — notifies group members`, 'POST', '/create_expense', body);
@@ -96,18 +115,21 @@ export function registerExpenseTools(server, client) {
96
115
  });
97
116
  server.registerTool('sw_update_expense', {
98
117
  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). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it updates the expense.',
99
- inputSchema: {
118
+ inputSchema: z.object({
100
119
  expense_id: z.number().describe('ID of the expense to update'),
101
120
  description: z.string().optional(),
102
121
  cost: z.string().describe('Decimal string, e.g. "25.00"').optional(),
103
122
  split_equally: z.boolean().describe('Mutually exclusive with users').optional(),
104
- users: z.array(userShareSchema).describe('Full replacement split — all users must be included. Mutually exclusive with split_equally.').optional(),
123
+ users: z
124
+ .array(userShareSchema)
125
+ .describe('Full replacement split — all users must be included. Mutually exclusive with split_equally.')
126
+ .optional(),
105
127
  currency_code: z.string().optional(),
106
128
  date: z.string().optional(),
107
129
  category_id: z.number().optional(),
108
130
  details: z.string().optional(),
109
131
  confirm: schemaConfirm,
110
- },
132
+ }),
111
133
  }, async (args) => {
112
134
  const { expense_id } = args;
113
135
  const body = buildExpenseBody(args);
@@ -120,10 +142,10 @@ export function registerExpenseTools(server, client) {
120
142
  server.registerTool('sw_delete_expense', {
121
143
  description: 'Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.',
122
144
  annotations: { destructiveHint: true },
123
- inputSchema: {
145
+ inputSchema: z.object({
124
146
  id: z.number().describe('Expense ID to delete'),
125
147
  confirm: schemaConfirm,
126
- },
148
+ }),
127
149
  }, async ({ id, confirm }) => {
128
150
  const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise expense ${id}`, 'POST', `/delete_expense/${id}`);
129
151
  if (gate)
@@ -133,9 +155,9 @@ export function registerExpenseTools(server, client) {
133
155
  });
134
156
  server.registerTool('sw_undelete_expense', {
135
157
  description: 'Restore a soft-deleted Splitwise expense.',
136
- inputSchema: {
158
+ inputSchema: z.object({
137
159
  id: z.number().describe('Expense ID to restore'),
138
- },
160
+ }),
139
161
  }, async ({ id }) => {
140
162
  const data = await client.request('POST', `/undelete_expense/${id}`);
141
163
  return minifiedResult(data);
@@ -6,9 +6,9 @@ export function registerFriendTools(server, client) {
6
6
  server.registerTool('sw_list_friends', {
7
7
  description: "List all Splitwise friends. Use this to resolve a friend's name to a user_id before adding them to a group or building a custom expense split. The compact default returns id, name (first_name + last_name joined), email, registration_status and any non-empty balance per friend; pass view:'full' for Splitwise's raw records, which keep first_name and last_name separate.",
8
8
  annotations: { readOnlyHint: true },
9
- inputSchema: {
9
+ inputSchema: z.object({
10
10
  view: viewParam(SW_VIEWS, { note: PERSON_VIEW_NOTE }),
11
- },
11
+ }),
12
12
  }, async ({ view }) => {
13
13
  const data = await client.request('GET', '/get_friends');
14
14
  return minifiedResult(viewFriends(resolveView(view, SW_VIEWS), data));
@@ -16,12 +16,12 @@ export function registerFriendTools(server, client) {
16
16
  server.registerTool('sw_create_friend', {
17
17
  description: 'Add a Splitwise friend by email (sends them an invite). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it adds the friend.',
18
18
  annotations: { destructiveHint: true },
19
- inputSchema: {
19
+ inputSchema: z.object({
20
20
  user_email: z.string().describe('Email of the user to add as a friend'),
21
21
  user_first_name: z.string().describe('First name of the user').optional(),
22
22
  user_last_name: z.string().describe('Last name of the user').optional(),
23
23
  confirm: schemaConfirm,
24
- },
24
+ }),
25
25
  }, async ({ user_email, user_first_name, user_last_name, confirm }) => {
26
26
  const body = pruneUndefined({ user_email, user_first_name, user_last_name });
27
27
  const gate = previewUnlessConfirmed(confirm, `Add ${user_email} as a Splitwise friend`, 'POST', '/create_friend', body);
@@ -33,10 +33,10 @@ export function registerFriendTools(server, client) {
33
33
  server.registerTool('sw_delete_friend', {
34
34
  description: 'Remove a Splitwise friendship by user id. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it removes the friendship.',
35
35
  annotations: { destructiveHint: true },
36
- inputSchema: {
36
+ inputSchema: z.object({
37
37
  id: z.number().describe('User ID of the friend to remove'),
38
38
  confirm: schemaConfirm,
39
- },
39
+ }),
40
40
  }, async ({ id, confirm }) => {
41
41
  const gate = previewUnlessConfirmed(confirm, `Remove Splitwise friendship with user ${id}`, 'POST', `/delete_friend/${id}`);
42
42
  if (gate)
@@ -6,9 +6,11 @@ export function registerGroupTools(server, client) {
6
6
  server.registerTool('sw_list_groups', {
7
7
  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.',
8
8
  annotations: { readOnlyHint: true },
9
- inputSchema: {
10
- view: viewParam(SW_VIEWS, { note: 'compact drops the avatar/cover-photo URLs (60% of a live 51-group response, which does not fit in a tool result at all) and the whiteboard/reminder settings; "full" returns Splitwise\'s whole records.' }),
11
- },
9
+ inputSchema: z.object({
10
+ view: viewParam(SW_VIEWS, {
11
+ note: 'compact drops the avatar/cover-photo URLs (60% of a live 51-group response, which does not fit in a tool result at all) and the whiteboard/reminder settings; "full" returns Splitwise\'s whole records.',
12
+ }),
13
+ }),
12
14
  }, async ({ view }) => {
13
15
  const data = await client.request('GET', '/get_groups');
14
16
  return minifiedResult(viewGroups(resolveView(view, SW_VIEWS), data));
@@ -16,10 +18,12 @@ export function registerGroupTools(server, client) {
16
18
  server.registerTool('sw_get_group', {
17
19
  description: 'Get details of a single Splitwise group including all members and balances.',
18
20
  annotations: { readOnlyHint: true },
19
- inputSchema: {
20
- view: viewParam(SW_VIEWS, { note: 'compact drops the avatar/cover-photo URLs (60% of a live 51-group response, which does not fit in a tool result at all) and the whiteboard/reminder settings; "full" returns Splitwise\'s whole records.' }),
21
+ inputSchema: z.object({
22
+ view: viewParam(SW_VIEWS, {
23
+ note: 'compact drops the avatar/cover-photo URLs (60% of a live 51-group response, which does not fit in a tool result at all) and the whiteboard/reminder settings; "full" returns Splitwise\'s whole records.',
24
+ }),
21
25
  id: z.number().describe('Group ID'),
22
- },
26
+ }),
23
27
  }, async ({ id, view }) => {
24
28
  const data = await client.request('GET', `/get_group/${id}`);
25
29
  return minifiedResult(viewGroup(resolveView(view, SW_VIEWS), data));
@@ -27,12 +31,18 @@ export function registerGroupTools(server, client) {
27
31
  server.registerTool('sw_create_group', {
28
32
  description: 'Create a new Splitwise group.',
29
33
  annotations: { destructiveHint: true },
30
- inputSchema: {
34
+ inputSchema: z.object({
31
35
  name: z.string().describe('Group name'),
32
- group_type: z.enum(['apartment', 'house', 'trip', 'other']).describe('Type of group').optional(),
33
- simplify_by_default: z.boolean().describe('Whether to simplify debts by default').optional(),
36
+ group_type: z
37
+ .enum(['apartment', 'house', 'trip', 'other'])
38
+ .describe('Type of group')
39
+ .optional(),
40
+ simplify_by_default: z
41
+ .boolean()
42
+ .describe('Whether to simplify debts by default')
43
+ .optional(),
34
44
  confirm: schemaConfirm,
35
- },
45
+ }),
36
46
  }, async ({ name, group_type, simplify_by_default, confirm }) => {
37
47
  const body = pruneUndefined({ name, group_type, simplify_by_default });
38
48
  const gate = previewUnlessConfirmed(confirm, `Create Splitwise group "${name}"`, 'POST', '/create_group', body);
@@ -42,15 +52,15 @@ export function registerGroupTools(server, client) {
42
52
  return minifiedResult(data);
43
53
  });
44
54
  server.registerTool('sw_add_user_to_group', {
45
- 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.",
46
- inputSchema: {
55
+ 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.',
56
+ inputSchema: z.object({
47
57
  group_id: z.number().describe('Group ID'),
48
58
  user_id: z.number().describe('User ID (preferred)').optional(),
49
59
  first_name: z.string().optional(),
50
60
  last_name: z.string().optional(),
51
61
  email: z.string().optional(),
52
62
  confirm: schemaConfirm,
53
- },
63
+ }),
54
64
  }, async ({ group_id, user_id, first_name, last_name, email, confirm }) => {
55
65
  let body;
56
66
  if (user_id !== undefined) {
@@ -71,11 +81,11 @@ export function registerGroupTools(server, client) {
71
81
  server.registerTool('sw_remove_user_from_group', {
72
82
  description: 'Remove a user from a Splitwise group. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it removes the user.',
73
83
  annotations: { destructiveHint: true },
74
- inputSchema: {
84
+ inputSchema: z.object({
75
85
  group_id: z.number().describe('Group ID'),
76
86
  user_id: z.number().describe('User ID to remove'),
77
87
  confirm: schemaConfirm,
78
- },
88
+ }),
79
89
  }, async ({ group_id, user_id, confirm }) => {
80
90
  const gate = previewUnlessConfirmed(confirm, `Remove user ${user_id} from Splitwise group ${group_id}`, 'POST', '/remove_user_from_group', { group_id, user_id });
81
91
  if (gate)
@@ -86,10 +96,10 @@ export function registerGroupTools(server, client) {
86
96
  server.registerTool('sw_delete_group', {
87
97
  description: 'Soft-delete a Splitwise group. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.',
88
98
  annotations: { destructiveHint: true },
89
- inputSchema: {
99
+ inputSchema: z.object({
90
100
  id: z.number().describe('Group ID to delete'),
91
101
  confirm: schemaConfirm,
92
- },
102
+ }),
93
103
  }, async ({ id, confirm }) => {
94
104
  const gate = previewUnlessConfirmed(confirm, `Soft-delete Splitwise group ${id}`, 'POST', `/delete_group/${id}`);
95
105
  if (gate)
@@ -99,9 +109,9 @@ export function registerGroupTools(server, client) {
99
109
  });
100
110
  server.registerTool('sw_undelete_group', {
101
111
  description: 'Restore a soft-deleted Splitwise group.',
102
- inputSchema: {
112
+ inputSchema: z.object({
103
113
  id: z.number().describe('Group ID to restore'),
104
- },
114
+ }),
105
115
  }, async ({ id }) => {
106
116
  const data = await client.request('POST', `/undelete_group/${id}`);
107
117
  return minifiedResult(data);
@@ -81,7 +81,7 @@ export function registerReceiptTools(server, client) {
81
81
  // prompt. Not destructive either — `uniquePath` always picks a filename
82
82
  // that doesn't exist, so an existing file is never touched.
83
83
  annotations: { readOnlyHint: false, destructiveHint: false },
84
- inputSchema: {
84
+ inputSchema: z.object({
85
85
  id: z.number().describe('Expense ID (the same id sw_get_expense takes)'),
86
86
  size: z
87
87
  .enum(['original', 'large'])
@@ -101,9 +101,9 @@ export function registerReceiptTools(server, client) {
101
101
  .optional(),
102
102
  write: z
103
103
  .boolean()
104
- .describe('Write the receipt to disk. Defaults to true. Pass false when the caller cannot reach this server\'s filesystem, or when the server runs read-only.')
104
+ .describe("Write the receipt to disk. Defaults to true. Pass false when the caller cannot reach this server's filesystem, or when the server runs read-only.")
105
105
  .optional(),
106
- },
106
+ }),
107
107
  }, async ({ id, size, inline, extract_text, output_dir, write }) => {
108
108
  const wanted = size ?? 'original';
109
109
  const fallback = wanted === 'original' ? 'large' : 'original';
@@ -214,7 +214,10 @@ export function registerReceiptTools(server, client) {
214
214
  expense_id: id,
215
215
  size: served,
216
216
  ...(served !== wanted
217
- ? { requested_size: wanted, note: `Expense ${id} has no ${wanted} rendition; fetched ${served}.` }
217
+ ? {
218
+ requested_size: wanted,
219
+ note: `Expense ${id} has no ${wanted} rendition; fetched ${served}.`,
220
+ }
218
221
  : {}),
219
222
  ...(path !== undefined ? { path } : {}),
220
223
  ...(writeError !== undefined ? { write_error: writeError } : {}),
@@ -6,9 +6,9 @@ export function registerUserTools(server, client) {
6
6
  server.registerTool('sw_get_current_user', {
7
7
  description: "Get the authenticated Splitwise user's profile. Use the returned id when building custom expense splits. The compact default returns id, name (first_name + last_name joined), email and registration_status; pass view:'full' for Splitwise's raw record, which keeps first_name and last_name separate — the form sw_update_user takes.",
8
8
  annotations: { readOnlyHint: true },
9
- inputSchema: {
9
+ inputSchema: z.object({
10
10
  view: viewParam(SW_VIEWS, { note: PERSON_VIEW_NOTE }),
11
- },
11
+ }),
12
12
  }, async ({ view }) => {
13
13
  const data = await client.request('GET', '/get_current_user');
14
14
  return minifiedResult(viewUser(resolveView(view, SW_VIEWS), data));
@@ -16,17 +16,17 @@ export function registerUserTools(server, client) {
16
16
  server.registerTool('sw_get_user', {
17
17
  description: "Get another Splitwise user's profile by id. Same shape as sw_get_current_user: the compact default merges first_name/last_name into name, and view:'full' keeps them separate.",
18
18
  annotations: { readOnlyHint: true },
19
- inputSchema: {
19
+ inputSchema: z.object({
20
20
  view: viewParam(SW_VIEWS, { note: PERSON_VIEW_NOTE }),
21
21
  id: z.number().describe('User ID'),
22
- },
22
+ }),
23
23
  }, async ({ id, view }) => {
24
24
  const data = await client.request('GET', `/get_user/${id}`);
25
25
  return minifiedResult(viewUser(resolveView(view, SW_VIEWS), data));
26
26
  });
27
27
  server.registerTool('sw_update_user', {
28
28
  description: "Update the current user's profile fields. id must be the current user's id.",
29
- inputSchema: {
29
+ inputSchema: z.object({
30
30
  id: z.number().describe("User ID (must be the current user's id)"),
31
31
  first_name: z.string().optional(),
32
32
  last_name: z.string().optional(),
@@ -35,9 +35,16 @@ export function registerUserTools(server, client) {
35
35
  locale: z.string().optional(),
36
36
  default_currency: z.string().optional(),
37
37
  confirm: schemaConfirm,
38
- },
38
+ }),
39
39
  }, async ({ id, first_name, last_name, email, password, locale, default_currency, confirm }) => {
40
- const body = pruneUndefined({ first_name, last_name, email, password, locale, default_currency });
40
+ const body = pruneUndefined({
41
+ first_name,
42
+ last_name,
43
+ email,
44
+ password,
45
+ locale,
46
+ default_currency,
47
+ });
41
48
  // Never echo the password in the dry-run preview.
42
49
  const previewBody = { ...body, ...(password !== undefined ? { password: '[hidden]' } : {}) };
43
50
  const gate = previewUnlessConfirmed(confirm, `Update current Splitwise user ${id} profile`, 'POST', `/update_user/${id}`, previewBody);
@@ -6,9 +6,11 @@ export function registerUtilityTools(server, client) {
6
6
  server.registerTool('sw_get_notifications', {
7
7
  description: 'Get recent Splitwise activity notifications for the current user.',
8
8
  annotations: { readOnlyHint: true },
9
- inputSchema: {
10
- view: viewParam(SW_VIEWS, { note: 'compact drops the avatar URLs; "full" returns Splitwise\'s whole record.' }),
11
- },
9
+ inputSchema: z.object({
10
+ view: viewParam(SW_VIEWS, {
11
+ note: 'compact drops the avatar URLs; "full" returns Splitwise\'s whole record.',
12
+ }),
13
+ }),
12
14
  }, async ({ view }) => {
13
15
  const data = await client.request('GET', '/get_notifications');
14
16
  return minifiedResult(viewGeneric(resolveView(view, SW_VIEWS), data));
@@ -30,10 +32,12 @@ export function registerUtilityTools(server, client) {
30
32
  server.registerTool('sw_get_comments', {
31
33
  description: 'Get all comments on a Splitwise expense.',
32
34
  annotations: { readOnlyHint: true },
33
- inputSchema: {
34
- view: viewParam(SW_VIEWS, { note: 'compact drops the avatar URLs; "full" returns Splitwise\'s whole record.' }),
35
+ inputSchema: z.object({
36
+ view: viewParam(SW_VIEWS, {
37
+ note: 'compact drops the avatar URLs; "full" returns Splitwise\'s whole record.',
38
+ }),
35
39
  expense_id: z.number().describe('Expense ID to get comments for'),
36
- },
40
+ }),
37
41
  }, async ({ expense_id, view }) => {
38
42
  // buildQueryString percent-encodes the value — defense-in-depth against
39
43
  // query-param injection (already constrained to a number by the schema).
@@ -43,11 +47,11 @@ export function registerUtilityTools(server, client) {
43
47
  server.registerTool('sw_create_comment', {
44
48
  description: 'Add a comment to a Splitwise expense (visible to other participants). Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it posts.',
45
49
  annotations: { destructiveHint: true },
46
- inputSchema: {
50
+ inputSchema: z.object({
47
51
  expense_id: z.number().describe('Expense ID to comment on'),
48
52
  content: z.string().describe('Comment text'),
49
53
  confirm: schemaConfirm,
50
- },
54
+ }),
51
55
  }, async ({ expense_id, content, confirm }) => {
52
56
  const gate = previewUnlessConfirmed(confirm, `Comment on Splitwise expense ${expense_id} (visible to participants)`, 'POST', '/create_comment', { expense_id, content });
53
57
  if (gate)
@@ -58,10 +62,10 @@ export function registerUtilityTools(server, client) {
58
62
  server.registerTool('sw_delete_comment', {
59
63
  description: 'Delete a comment by id. Without confirm:true this returns a dry-run preview and makes NO network call; with confirm:true it deletes.',
60
64
  annotations: { destructiveHint: true },
61
- inputSchema: {
65
+ inputSchema: z.object({
62
66
  id: z.number().describe('Comment ID to delete'),
63
67
  confirm: schemaConfirm,
64
- },
68
+ }),
65
69
  }, async ({ id, confirm }) => {
66
70
  const gate = previewUnlessConfirmed(confirm, `Delete Splitwise comment ${id}`, 'POST', `/delete_comment/${id}`);
67
71
  if (gate)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
- "version": "2.4.2",
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>",
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "build": "tsc && npm run bundle",
40
- "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
40
+ "bundle": "esbuild src/index.ts --bundle --alias:zod/v4=./node_modules/zod/v4/index.cjs --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
41
41
  "dev": "node dist/index.js",
42
42
  "test": "npm run typecheck && vitest run",
43
43
  "test:watch": "vitest",
@@ -45,13 +45,14 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^0.26.1",
49
- "@modelcontextprotocol/sdk": "^1.30.0",
48
+ "@chrischall/mcp-utils": "^1.0.0",
49
+ "@modelcontextprotocol/server": "^2.0.0",
50
50
  "dotenv": "^17.4.0",
51
51
  "unpdf": "^1.8.1",
52
- "zod": "^4.5.4"
52
+ "zod": "^4.6.2"
53
53
  },
54
54
  "devDependencies": {
55
+ "@modelcontextprotocol/client": "^2.0.0",
55
56
  "@types/node": "^26.0.0",
56
57
  "@vitest/coverage-v8": "^5.0.0",
57
58
  "esbuild": "^0.28.0",
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.4.2",
9
+ "version": "3.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "splitwise-mcp",
14
- "version": "2.4.2",
14
+ "version": "3.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },