splitwise-mcp 1.3.0 → 2.0.1
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/bundle.js +9961 -886
- package/dist/index.js +12 -47
- package/dist/tools/expenses.js +77 -139
- package/dist/tools/friends.js +27 -49
- package/dist/tools/groups.js +66 -120
- package/dist/tools/user.js +41 -63
- package/dist/tools/utilities.js +36 -71
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -1,54 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
4
|
import { SplitwiseClient } from './client.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import { registerUserTools } from './tools/user.js';
|
|
6
|
+
import { registerGroupTools } from './tools/groups.js';
|
|
7
|
+
import { registerFriendTools } from './tools/friends.js';
|
|
8
|
+
import { registerExpenseTools } from './tools/expenses.js';
|
|
9
|
+
import { registerUtilityTools } from './tools/utilities.js';
|
|
11
10
|
const client = new SplitwiseClient();
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
];
|
|
19
|
-
const handlers = {};
|
|
20
|
-
for (const tool of userTools)
|
|
21
|
-
handlers[tool.name] = (n, a) => handleUser(n, a, client);
|
|
22
|
-
for (const tool of groupTools)
|
|
23
|
-
handlers[tool.name] = (n, a) => handleGroups(n, a, client);
|
|
24
|
-
for (const tool of friendTools)
|
|
25
|
-
handlers[tool.name] = (n, a) => handleFriends(n, a, client);
|
|
26
|
-
for (const tool of expenseTools)
|
|
27
|
-
handlers[tool.name] = (n, a) => handleExpenses(n, a, client);
|
|
28
|
-
for (const tool of utilityTools)
|
|
29
|
-
handlers[tool.name] = (n, a) => handleUtilities(n, a, client);
|
|
30
|
-
const server = new Server({ name: 'splitwise-mcp', version: '1.2.0' }, { capabilities: { tools: {} } });
|
|
31
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
|
|
32
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
33
|
-
const { name, arguments: args = {} } = request.params;
|
|
34
|
-
const handler = handlers[name];
|
|
35
|
-
if (!handler) {
|
|
36
|
-
return {
|
|
37
|
-
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
38
|
-
isError: true,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
return await handler(name, args);
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
48
|
-
isError: true,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
});
|
|
11
|
+
const server = new McpServer({ name: 'splitwise-mcp', version: '2.0.2' });
|
|
12
|
+
registerUserTools(server, client);
|
|
13
|
+
registerGroupTools(server, client);
|
|
14
|
+
registerFriendTools(server, client);
|
|
15
|
+
registerExpenseTools(server, client);
|
|
16
|
+
registerUtilityTools(server, client);
|
|
52
17
|
console.error('[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.');
|
|
53
18
|
const transport = new StdioServerTransport();
|
|
54
19
|
await server.connect(transport);
|
package/dist/tools/expenses.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
/** Flattens a users array into Splitwise's flat-param JSON format. */
|
|
2
3
|
export function flattenUsers(users) {
|
|
3
4
|
const flat = {};
|
|
@@ -22,164 +23,101 @@ function buildExpenseBody(args) {
|
|
|
22
23
|
}
|
|
23
24
|
return body;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const userShareSchema = z.object({
|
|
27
|
+
user_id: z.number(),
|
|
28
|
+
paid_share: z.string().describe('Amount this user paid, e.g. "25.00"'),
|
|
29
|
+
owed_share: z.string().describe('Amount this user owes, e.g. "12.50"'),
|
|
30
|
+
});
|
|
31
|
+
export function registerExpenseTools(server, client) {
|
|
32
|
+
server.registerTool('sw_list_expenses', {
|
|
28
33
|
description: 'List or search Splitwise expenses. All filters are optional. Use group_id to filter by group, dated_after/dated_before for date ranges.',
|
|
29
34
|
annotations: { readOnlyHint: true },
|
|
30
35
|
inputSchema: {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
limit: { type: 'integer', description: 'Max results (API default: 20)' },
|
|
40
|
-
offset: { type: 'integer', description: 'Pagination offset' },
|
|
41
|
-
},
|
|
42
|
-
required: [],
|
|
36
|
+
group_id: z.number().describe('Only expenses in this group').optional(),
|
|
37
|
+
friend_id: z.number().describe('Only expenses with this friend').optional(),
|
|
38
|
+
dated_after: z.string().describe('ISO 8601 date — only expenses on or after this date').optional(),
|
|
39
|
+
dated_before: z.string().describe('ISO 8601 date — only expenses on or before this date').optional(),
|
|
40
|
+
updated_after: z.string().describe('ISO 8601 datetime').optional(),
|
|
41
|
+
updated_before: z.string().describe('ISO 8601 datetime').optional(),
|
|
42
|
+
limit: z.number().describe('Max results (API default: 20)').optional(),
|
|
43
|
+
offset: z.number().describe('Pagination offset').optional(),
|
|
43
44
|
},
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
}, async (args) => {
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
const filters = ['group_id', 'friend_id', 'dated_after', 'dated_before', 'updated_after', 'updated_before', 'limit', 'offset'];
|
|
48
|
+
for (const key of filters) {
|
|
49
|
+
const val = args[key];
|
|
50
|
+
if (val !== undefined)
|
|
51
|
+
params.append(key, String(val));
|
|
52
|
+
}
|
|
53
|
+
const qs = params.toString();
|
|
54
|
+
const data = await client.request('GET', qs ? `/get_expenses?${qs}` : '/get_expenses');
|
|
55
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
56
|
+
});
|
|
57
|
+
server.registerTool('sw_get_expense', {
|
|
47
58
|
description: 'Get full details of a single Splitwise expense by id.',
|
|
48
59
|
annotations: { readOnlyHint: true },
|
|
49
60
|
inputSchema: {
|
|
50
|
-
|
|
51
|
-
properties: {
|
|
52
|
-
id: { type: 'integer', description: 'Expense ID' },
|
|
53
|
-
},
|
|
54
|
-
required: ['id'],
|
|
61
|
+
id: z.number().describe('Expense ID'),
|
|
55
62
|
},
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
}, async ({ id }) => {
|
|
64
|
+
const data = await client.request('GET', `/get_expense/${id}`);
|
|
65
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
66
|
+
});
|
|
67
|
+
server.registerTool('sw_create_expense', {
|
|
59
68
|
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.',
|
|
60
69
|
inputSchema: {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
items: {
|
|
71
|
-
type: 'object',
|
|
72
|
-
properties: {
|
|
73
|
-
user_id: { type: 'integer' },
|
|
74
|
-
paid_share: { type: 'string', description: 'Amount this user paid, e.g. "25.00"' },
|
|
75
|
-
owed_share: { type: 'string', description: 'Amount this user owes, e.g. "12.50"' },
|
|
76
|
-
},
|
|
77
|
-
required: ['user_id', 'paid_share', 'owed_share'],
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
currency_code: { type: 'string', description: 'Currency code, e.g. "USD". Defaults to group/user default.' },
|
|
81
|
-
date: { type: 'string', description: 'ISO 8601 datetime' },
|
|
82
|
-
category_id: { type: 'integer', description: 'Category id from sw_get_categories' },
|
|
83
|
-
details: { type: 'string', description: 'Notes' },
|
|
84
|
-
},
|
|
85
|
-
required: ['group_id', 'description', 'cost'],
|
|
70
|
+
group_id: z.number().describe('Group to add expense to (use 0 for no group)'),
|
|
71
|
+
description: z.string().describe('Short description of the expense'),
|
|
72
|
+
cost: z.string().describe('Total cost as decimal string, e.g. "25.00"'),
|
|
73
|
+
split_equally: z.boolean().describe('Split equally among group members (mutually exclusive with users)').optional(),
|
|
74
|
+
users: z.array(userShareSchema).describe('Custom split (mutually exclusive with split_equally). Full list of participants required.').optional(),
|
|
75
|
+
currency_code: z.string().describe('Currency code, e.g. "USD". Defaults to group/user default.').optional(),
|
|
76
|
+
date: z.string().describe('ISO 8601 datetime').optional(),
|
|
77
|
+
category_id: z.number().describe('Category id from sw_get_categories').optional(),
|
|
78
|
+
details: z.string().describe('Notes').optional(),
|
|
86
79
|
},
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
}, async (args) => {
|
|
81
|
+
const body = buildExpenseBody(args);
|
|
82
|
+
const data = await client.request('POST', '/create_expense', body);
|
|
83
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
84
|
+
});
|
|
85
|
+
server.registerTool('sw_update_expense', {
|
|
90
86
|
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).',
|
|
91
87
|
inputSchema: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
items: {
|
|
102
|
-
type: 'object',
|
|
103
|
-
properties: {
|
|
104
|
-
user_id: { type: 'integer' },
|
|
105
|
-
paid_share: { type: 'string' },
|
|
106
|
-
owed_share: { type: 'string' },
|
|
107
|
-
},
|
|
108
|
-
required: ['user_id', 'paid_share', 'owed_share'],
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
currency_code: { type: 'string' },
|
|
112
|
-
date: { type: 'string' },
|
|
113
|
-
category_id: { type: 'integer' },
|
|
114
|
-
details: { type: 'string' },
|
|
115
|
-
},
|
|
116
|
-
required: ['expense_id'],
|
|
88
|
+
expense_id: z.number().describe('ID of the expense to update'),
|
|
89
|
+
description: z.string().optional(),
|
|
90
|
+
cost: z.string().describe('Decimal string, e.g. "25.00"').optional(),
|
|
91
|
+
split_equally: z.boolean().describe('Mutually exclusive with users').optional(),
|
|
92
|
+
users: z.array(userShareSchema).describe('Full replacement split — all users must be included. Mutually exclusive with split_equally.').optional(),
|
|
93
|
+
currency_code: z.string().optional(),
|
|
94
|
+
date: z.string().optional(),
|
|
95
|
+
category_id: z.number().optional(),
|
|
96
|
+
details: z.string().optional(),
|
|
117
97
|
},
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
}, async (args) => {
|
|
99
|
+
const { expense_id } = args;
|
|
100
|
+
const body = buildExpenseBody(args);
|
|
101
|
+
const data = await client.request('POST', `/update_expense/${expense_id}`, body);
|
|
102
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
103
|
+
});
|
|
104
|
+
server.registerTool('sw_delete_expense', {
|
|
121
105
|
description: 'Soft-delete a Splitwise expense by id. Returns {success: true} on success. Use sw_undelete_expense to restore.',
|
|
122
106
|
annotations: { destructiveHint: true },
|
|
123
107
|
inputSchema: {
|
|
124
|
-
|
|
125
|
-
properties: {
|
|
126
|
-
id: { type: 'integer', description: 'Expense ID to delete' },
|
|
127
|
-
},
|
|
128
|
-
required: ['id'],
|
|
108
|
+
id: z.number().describe('Expense ID to delete'),
|
|
129
109
|
},
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
}, async ({ id }) => {
|
|
111
|
+
const data = await client.request('POST', `/delete_expense/${id}`);
|
|
112
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
113
|
+
});
|
|
114
|
+
server.registerTool('sw_undelete_expense', {
|
|
133
115
|
description: 'Restore a soft-deleted Splitwise expense.',
|
|
134
116
|
inputSchema: {
|
|
135
|
-
|
|
136
|
-
properties: {
|
|
137
|
-
id: { type: 'integer', description: 'Expense ID to restore' },
|
|
138
|
-
},
|
|
139
|
-
required: ['id'],
|
|
117
|
+
id: z.number().describe('Expense ID to restore'),
|
|
140
118
|
},
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case 'sw_list_expenses': {
|
|
146
|
-
const params = new URLSearchParams();
|
|
147
|
-
const filters = ['group_id', 'friend_id', 'dated_after', 'dated_before', 'updated_after', 'updated_before', 'limit', 'offset'];
|
|
148
|
-
for (const key of filters) {
|
|
149
|
-
if (args[key] !== undefined)
|
|
150
|
-
params.append(key, String(args[key]));
|
|
151
|
-
}
|
|
152
|
-
const qs = params.toString();
|
|
153
|
-
const data = await client.request('GET', qs ? `/get_expenses?${qs}` : '/get_expenses');
|
|
154
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
155
|
-
}
|
|
156
|
-
case 'sw_get_expense': {
|
|
157
|
-
const { id } = args;
|
|
158
|
-
const data = await client.request('GET', `/get_expense/${id}`);
|
|
159
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
160
|
-
}
|
|
161
|
-
case 'sw_create_expense': {
|
|
162
|
-
const body = buildExpenseBody(args);
|
|
163
|
-
const data = await client.request('POST', '/create_expense', body);
|
|
164
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
165
|
-
}
|
|
166
|
-
case 'sw_update_expense': {
|
|
167
|
-
const { expense_id } = args;
|
|
168
|
-
const body = buildExpenseBody(args);
|
|
169
|
-
const data = await client.request('POST', `/update_expense/${expense_id}`, body);
|
|
170
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
171
|
-
}
|
|
172
|
-
case 'sw_delete_expense': {
|
|
173
|
-
const { id } = args;
|
|
174
|
-
const data = await client.request('POST', `/delete_expense/${id}`);
|
|
175
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
176
|
-
}
|
|
177
|
-
case 'sw_undelete_expense': {
|
|
178
|
-
const { id } = args;
|
|
179
|
-
const data = await client.request('POST', `/undelete_expense/${id}`);
|
|
180
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
181
|
-
}
|
|
182
|
-
default:
|
|
183
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
184
|
-
}
|
|
119
|
+
}, async ({ id }) => {
|
|
120
|
+
const data = await client.request('POST', `/undelete_expense/${id}`);
|
|
121
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
122
|
+
});
|
|
185
123
|
}
|
package/dist/tools/friends.js
CHANGED
|
@@ -1,58 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
description:
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerFriendTools(server, client) {
|
|
3
|
+
server.registerTool('sw_list_friends', {
|
|
4
|
+
description: "List all Splitwise friends with their id, first_name, last_name, and email. Use this to resolve a friend's name to a user_id before adding them to a group or building a custom expense split.",
|
|
5
5
|
annotations: { readOnlyHint: true },
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
}, async () => {
|
|
7
|
+
const data = await client.request('GET', '/get_friends');
|
|
8
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
9
|
+
});
|
|
10
|
+
server.registerTool('sw_create_friend', {
|
|
10
11
|
description: 'Add a Splitwise friend by email.',
|
|
11
12
|
inputSchema: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
user_first_name: { type: 'string', description: 'First name of the user' },
|
|
16
|
-
user_last_name: { type: 'string', description: 'Last name of the user' },
|
|
17
|
-
},
|
|
18
|
-
required: ['user_email'],
|
|
13
|
+
user_email: z.string().describe('Email of the user to add as a friend'),
|
|
14
|
+
user_first_name: z.string().describe('First name of the user').optional(),
|
|
15
|
+
user_last_name: z.string().describe('Last name of the user').optional(),
|
|
19
16
|
},
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
}, async ({ user_email, user_first_name, user_last_name }) => {
|
|
18
|
+
const body = { user_email };
|
|
19
|
+
if (user_first_name !== undefined)
|
|
20
|
+
body.user_first_name = user_first_name;
|
|
21
|
+
if (user_last_name !== undefined)
|
|
22
|
+
body.user_last_name = user_last_name;
|
|
23
|
+
const data = await client.request('POST', '/create_friend', body);
|
|
24
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
25
|
+
});
|
|
26
|
+
server.registerTool('sw_delete_friend', {
|
|
23
27
|
description: 'Remove a Splitwise friendship by user id.',
|
|
24
28
|
annotations: { destructiveHint: true },
|
|
25
29
|
inputSchema: {
|
|
26
|
-
|
|
27
|
-
properties: {
|
|
28
|
-
id: { type: 'integer', description: 'User ID of the friend to remove' },
|
|
29
|
-
},
|
|
30
|
-
required: ['id'],
|
|
30
|
+
id: z.number().describe('User ID of the friend to remove'),
|
|
31
31
|
},
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
case 'sw_list_friends': {
|
|
37
|
-
const data = await client.request('GET', '/get_friends');
|
|
38
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
39
|
-
}
|
|
40
|
-
case 'sw_create_friend': {
|
|
41
|
-
const { user_email, user_first_name, user_last_name } = args;
|
|
42
|
-
const body = { user_email };
|
|
43
|
-
if (user_first_name !== undefined)
|
|
44
|
-
body.user_first_name = user_first_name;
|
|
45
|
-
if (user_last_name !== undefined)
|
|
46
|
-
body.user_last_name = user_last_name;
|
|
47
|
-
const data = await client.request('POST', '/create_friend', body);
|
|
48
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
49
|
-
}
|
|
50
|
-
case 'sw_delete_friend': {
|
|
51
|
-
const { id } = args;
|
|
52
|
-
const data = await client.request('POST', `/delete_friend/${id}`);
|
|
53
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
54
|
-
}
|
|
55
|
-
default:
|
|
56
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
57
|
-
}
|
|
32
|
+
}, async ({ id }) => {
|
|
33
|
+
const data = await client.request('POST', `/delete_friend/${id}`);
|
|
34
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
35
|
+
});
|
|
58
36
|
}
|
package/dist/tools/groups.js
CHANGED
|
@@ -1,142 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerGroupTools(server, client) {
|
|
3
|
+
server.registerTool('sw_list_groups', {
|
|
4
4
|
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.',
|
|
5
5
|
annotations: { readOnlyHint: true },
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
}, async () => {
|
|
7
|
+
const data = await client.request('GET', '/get_groups');
|
|
8
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
9
|
+
});
|
|
10
|
+
server.registerTool('sw_get_group', {
|
|
10
11
|
description: 'Get details of a single Splitwise group including all members and balances.',
|
|
11
12
|
annotations: { readOnlyHint: true },
|
|
12
13
|
inputSchema: {
|
|
13
|
-
|
|
14
|
-
properties: {
|
|
15
|
-
id: { type: 'integer', description: 'Group ID' },
|
|
16
|
-
},
|
|
17
|
-
required: ['id'],
|
|
14
|
+
id: z.number().describe('Group ID'),
|
|
18
15
|
},
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
}, async ({ id }) => {
|
|
17
|
+
const data = await client.request('GET', `/get_group/${id}`);
|
|
18
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
19
|
+
});
|
|
20
|
+
server.registerTool('sw_create_group', {
|
|
22
21
|
description: 'Create a new Splitwise group.',
|
|
23
22
|
inputSchema: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
group_type: {
|
|
28
|
-
type: 'string',
|
|
29
|
-
enum: ['apartment', 'house', 'trip', 'other'],
|
|
30
|
-
description: 'Type of group',
|
|
31
|
-
},
|
|
32
|
-
simplify_by_default: { type: 'boolean', description: 'Whether to simplify debts by default' },
|
|
33
|
-
},
|
|
34
|
-
required: ['name'],
|
|
23
|
+
name: z.string().describe('Group name'),
|
|
24
|
+
group_type: z.enum(['apartment', 'house', 'trip', 'other']).describe('Type of group').optional(),
|
|
25
|
+
simplify_by_default: z.boolean().describe('Whether to simplify debts by default').optional(),
|
|
35
26
|
},
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
}, async ({ name, group_type, simplify_by_default }) => {
|
|
28
|
+
const body = { name };
|
|
29
|
+
if (group_type !== undefined)
|
|
30
|
+
body.group_type = group_type;
|
|
31
|
+
if (simplify_by_default !== undefined)
|
|
32
|
+
body.simplify_by_default = simplify_by_default;
|
|
33
|
+
const data = await client.request('POST', '/create_group', body);
|
|
34
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
35
|
+
});
|
|
36
|
+
server.registerTool('sw_add_user_to_group', {
|
|
37
|
+
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.",
|
|
40
38
|
inputSchema: {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
last_name: { type: 'string' },
|
|
47
|
-
email: { type: 'string' },
|
|
48
|
-
},
|
|
49
|
-
required: ['group_id'],
|
|
39
|
+
group_id: z.number().describe('Group ID'),
|
|
40
|
+
user_id: z.number().describe('User ID (preferred)').optional(),
|
|
41
|
+
first_name: z.string().optional(),
|
|
42
|
+
last_name: z.string().optional(),
|
|
43
|
+
email: z.string().optional(),
|
|
50
44
|
},
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
}, async ({ group_id, user_id, first_name, last_name, email }) => {
|
|
46
|
+
let body;
|
|
47
|
+
if (user_id !== undefined) {
|
|
48
|
+
body = { group_id, user_id };
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (!first_name || !last_name || !email) {
|
|
52
|
+
throw new Error('first_name, last_name, and email are required when user_id is not provided');
|
|
53
|
+
}
|
|
54
|
+
body = { group_id, first_name, last_name, email };
|
|
55
|
+
}
|
|
56
|
+
const data = await client.request('POST', '/add_user_to_group', body);
|
|
57
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
58
|
+
});
|
|
59
|
+
server.registerTool('sw_remove_user_from_group', {
|
|
54
60
|
description: 'Remove a user from a Splitwise group.',
|
|
55
61
|
inputSchema: {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
group_id: { type: 'integer', description: 'Group ID' },
|
|
59
|
-
user_id: { type: 'integer', description: 'User ID to remove' },
|
|
60
|
-
},
|
|
61
|
-
required: ['group_id', 'user_id'],
|
|
62
|
+
group_id: z.number().describe('Group ID'),
|
|
63
|
+
user_id: z.number().describe('User ID to remove'),
|
|
62
64
|
},
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
}, async ({ group_id, user_id }) => {
|
|
66
|
+
const data = await client.request('POST', '/remove_user_from_group', { group_id, user_id });
|
|
67
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
68
|
+
});
|
|
69
|
+
server.registerTool('sw_delete_group', {
|
|
66
70
|
description: 'Soft-delete a Splitwise group.',
|
|
67
71
|
annotations: { destructiveHint: true },
|
|
68
72
|
inputSchema: {
|
|
69
|
-
|
|
70
|
-
properties: {
|
|
71
|
-
id: { type: 'integer', description: 'Group ID to delete' },
|
|
72
|
-
},
|
|
73
|
-
required: ['id'],
|
|
73
|
+
id: z.number().describe('Group ID to delete'),
|
|
74
74
|
},
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
}, async ({ id }) => {
|
|
76
|
+
const data = await client.request('POST', `/delete_group/${id}`);
|
|
77
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
78
|
+
});
|
|
79
|
+
server.registerTool('sw_undelete_group', {
|
|
78
80
|
description: 'Restore a soft-deleted Splitwise group.',
|
|
79
81
|
inputSchema: {
|
|
80
|
-
|
|
81
|
-
properties: {
|
|
82
|
-
id: { type: 'integer', description: 'Group ID to restore' },
|
|
83
|
-
},
|
|
84
|
-
required: ['id'],
|
|
82
|
+
id: z.number().describe('Group ID to restore'),
|
|
85
83
|
},
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
case 'sw_list_groups': {
|
|
91
|
-
const data = await client.request('GET', '/get_groups');
|
|
92
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
93
|
-
}
|
|
94
|
-
case 'sw_get_group': {
|
|
95
|
-
const { id } = args;
|
|
96
|
-
const data = await client.request('GET', `/get_group/${id}`);
|
|
97
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
98
|
-
}
|
|
99
|
-
case 'sw_create_group': {
|
|
100
|
-
const { name, group_type, simplify_by_default } = args;
|
|
101
|
-
const body = { name };
|
|
102
|
-
if (group_type !== undefined)
|
|
103
|
-
body.group_type = group_type;
|
|
104
|
-
if (simplify_by_default !== undefined)
|
|
105
|
-
body.simplify_by_default = simplify_by_default;
|
|
106
|
-
const data = await client.request('POST', '/create_group', body);
|
|
107
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
108
|
-
}
|
|
109
|
-
case 'sw_add_user_to_group': {
|
|
110
|
-
const { group_id, user_id, first_name, last_name, email } = args;
|
|
111
|
-
let body;
|
|
112
|
-
if (user_id !== undefined) {
|
|
113
|
-
body = { group_id, user_id };
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
if (!first_name || !last_name || !email) {
|
|
117
|
-
throw new Error('first_name, last_name, and email are required when user_id is not provided');
|
|
118
|
-
}
|
|
119
|
-
body = { group_id, first_name, last_name, email };
|
|
120
|
-
}
|
|
121
|
-
const data = await client.request('POST', '/add_user_to_group', body);
|
|
122
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
123
|
-
}
|
|
124
|
-
case 'sw_remove_user_from_group': {
|
|
125
|
-
const { group_id, user_id } = args;
|
|
126
|
-
const data = await client.request('POST', '/remove_user_from_group', { group_id, user_id });
|
|
127
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
128
|
-
}
|
|
129
|
-
case 'sw_delete_group': {
|
|
130
|
-
const { id } = args;
|
|
131
|
-
const data = await client.request('POST', `/delete_group/${id}`);
|
|
132
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
133
|
-
}
|
|
134
|
-
case 'sw_undelete_group': {
|
|
135
|
-
const { id } = args;
|
|
136
|
-
const data = await client.request('POST', `/undelete_group/${id}`);
|
|
137
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
138
|
-
}
|
|
139
|
-
default:
|
|
140
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
141
|
-
}
|
|
84
|
+
}, async ({ id }) => {
|
|
85
|
+
const data = await client.request('POST', `/undelete_group/${id}`);
|
|
86
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
87
|
+
});
|
|
142
88
|
}
|