splitwise-mcp 1.0.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/client.js +44 -0
- package/dist/index.js +54 -0
- package/dist/tools/expenses.js +170 -0
- package/dist/tools/friends.js +20 -0
- package/dist/tools/groups.js +111 -0
- package/dist/tools/user.js +20 -0
- package/dist/tools/utilities.js +41 -0
- package/package.json +28 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { config as loadDotenv } from 'dotenv';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
loadDotenv({ path: join(__dirname, '..', '.env'), override: false, quiet: true });
|
|
6
|
+
const BASE_URL = 'https://secure.splitwise.com/api/v3.0';
|
|
7
|
+
export class SplitwiseClient {
|
|
8
|
+
apiKey;
|
|
9
|
+
constructor() {
|
|
10
|
+
const key = process.env.SPLITWISE_API_KEY;
|
|
11
|
+
if (!key)
|
|
12
|
+
throw new Error('SPLITWISE_API_KEY environment variable is required');
|
|
13
|
+
this.apiKey = key;
|
|
14
|
+
}
|
|
15
|
+
async request(method, path, body) {
|
|
16
|
+
return this.doRequest(method, path, body, false);
|
|
17
|
+
}
|
|
18
|
+
async doRequest(method, path, body, isRetry) {
|
|
19
|
+
const headers = {
|
|
20
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
Accept: 'application/json',
|
|
23
|
+
};
|
|
24
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
25
|
+
method,
|
|
26
|
+
headers,
|
|
27
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
28
|
+
});
|
|
29
|
+
if (response.status === 401) {
|
|
30
|
+
throw new Error('SPLITWISE_API_KEY is invalid or missing');
|
|
31
|
+
}
|
|
32
|
+
if (response.status === 429) {
|
|
33
|
+
if (!isRetry) {
|
|
34
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
35
|
+
return this.doRequest(method, path, body, true);
|
|
36
|
+
}
|
|
37
|
+
throw new Error('Rate limited by Splitwise API');
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`Splitwise API error: ${response.status} ${response.statusText} for ${method} ${path}`);
|
|
41
|
+
}
|
|
42
|
+
return response.json();
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { SplitwiseClient } from './client.js';
|
|
6
|
+
import { toolDefinitions as userTools, handleTool as handleUser } from './tools/user.js';
|
|
7
|
+
import { toolDefinitions as groupTools, handleTool as handleGroups } from './tools/groups.js';
|
|
8
|
+
import { toolDefinitions as friendTools, handleTool as handleFriends } from './tools/friends.js';
|
|
9
|
+
import { toolDefinitions as expenseTools, handleTool as handleExpenses } from './tools/expenses.js';
|
|
10
|
+
import { toolDefinitions as utilityTools, handleTool as handleUtilities } from './tools/utilities.js';
|
|
11
|
+
const client = new SplitwiseClient();
|
|
12
|
+
const allTools = [
|
|
13
|
+
...userTools,
|
|
14
|
+
...groupTools,
|
|
15
|
+
...friendTools,
|
|
16
|
+
...expenseTools,
|
|
17
|
+
...utilityTools,
|
|
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.0.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
|
+
});
|
|
52
|
+
console.error('[splitwise-mcp] This project was developed and is maintained by AI (Claude Sonnet 4.6). Use at your own discretion.');
|
|
53
|
+
const transport = new StdioServerTransport();
|
|
54
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Flattens a users array into Splitwise's flat-param JSON format. */
|
|
2
|
+
export function flattenUsers(users) {
|
|
3
|
+
const flat = {};
|
|
4
|
+
users.forEach((u, i) => {
|
|
5
|
+
flat[`users__${i}__user_id`] = u.user_id;
|
|
6
|
+
flat[`users__${i}__paid_share`] = u.paid_share;
|
|
7
|
+
flat[`users__${i}__owed_share`] = u.owed_share;
|
|
8
|
+
});
|
|
9
|
+
return flat;
|
|
10
|
+
}
|
|
11
|
+
function buildExpenseBody(args) {
|
|
12
|
+
const { split_equally, users, expense_id: _id, ...rest } = args;
|
|
13
|
+
if (split_equally && users) {
|
|
14
|
+
throw new Error('Provide either split_equally or users, not both');
|
|
15
|
+
}
|
|
16
|
+
const body = { ...rest };
|
|
17
|
+
if (split_equally) {
|
|
18
|
+
body.split_equally = true;
|
|
19
|
+
}
|
|
20
|
+
else if (users) {
|
|
21
|
+
Object.assign(body, flattenUsers(users));
|
|
22
|
+
}
|
|
23
|
+
return body;
|
|
24
|
+
}
|
|
25
|
+
export const toolDefinitions = [
|
|
26
|
+
{
|
|
27
|
+
name: 'sw_list_expenses',
|
|
28
|
+
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
|
+
annotations: { readOnlyHint: true },
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
group_id: { type: 'integer', description: 'Only expenses in this group' },
|
|
34
|
+
friend_id: { type: 'integer', description: 'Only expenses with this friend' },
|
|
35
|
+
dated_after: { type: 'string', description: 'ISO 8601 date — only expenses on or after this date' },
|
|
36
|
+
dated_before: { type: 'string', description: 'ISO 8601 date — only expenses on or before this date' },
|
|
37
|
+
updated_after: { type: 'string', description: 'ISO 8601 datetime' },
|
|
38
|
+
updated_before: { type: 'string', description: 'ISO 8601 datetime' },
|
|
39
|
+
limit: { type: 'integer', description: 'Max results (API default: 20)' },
|
|
40
|
+
offset: { type: 'integer', description: 'Pagination offset' },
|
|
41
|
+
},
|
|
42
|
+
required: [],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'sw_get_expense',
|
|
47
|
+
description: 'Get full details of a single Splitwise expense by id.',
|
|
48
|
+
annotations: { readOnlyHint: true },
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
id: { type: 'integer', description: 'Expense ID' },
|
|
53
|
+
},
|
|
54
|
+
required: ['id'],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'sw_create_expense',
|
|
59
|
+
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
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
group_id: { type: 'integer', description: 'Group to add expense to (use 0 for no group)' },
|
|
64
|
+
description: { type: 'string', description: 'Short description of the expense' },
|
|
65
|
+
cost: { type: 'string', description: 'Total cost as decimal string, e.g. "25.00"' },
|
|
66
|
+
split_equally: { type: 'boolean', description: 'Split equally among group members (mutually exclusive with users)' },
|
|
67
|
+
users: {
|
|
68
|
+
type: 'array',
|
|
69
|
+
description: 'Custom split (mutually exclusive with split_equally). Full list of participants required.',
|
|
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'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'sw_update_expense',
|
|
90
|
+
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
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
expense_id: { type: 'integer', description: 'ID of the expense to update' },
|
|
95
|
+
description: { type: 'string' },
|
|
96
|
+
cost: { type: 'string', description: 'Decimal string, e.g. "25.00"' },
|
|
97
|
+
split_equally: { type: 'boolean', description: 'Mutually exclusive with users' },
|
|
98
|
+
users: {
|
|
99
|
+
type: 'array',
|
|
100
|
+
description: 'Full replacement split — all users must be included. Mutually exclusive with split_equally.',
|
|
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'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'sw_delete_expense',
|
|
121
|
+
description: 'Soft-delete a Splitwise expense by id. Returns {success: true} on success. Note: restoring deleted expenses requires sw_undelete_expense (not yet implemented).',
|
|
122
|
+
annotations: { destructiveHint: true },
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
id: { type: 'integer', description: 'Expense ID to delete' },
|
|
127
|
+
},
|
|
128
|
+
required: ['id'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
// TODO: sw_undelete_expense — POST /undelete_expense/{id} — restore a soft-deleted expense
|
|
132
|
+
];
|
|
133
|
+
export async function handleTool(name, args, client) {
|
|
134
|
+
switch (name) {
|
|
135
|
+
case 'sw_list_expenses': {
|
|
136
|
+
const params = new URLSearchParams();
|
|
137
|
+
const filters = ['group_id', 'friend_id', 'dated_after', 'dated_before', 'updated_after', 'updated_before', 'limit', 'offset'];
|
|
138
|
+
for (const key of filters) {
|
|
139
|
+
if (args[key] !== undefined)
|
|
140
|
+
params.append(key, String(args[key]));
|
|
141
|
+
}
|
|
142
|
+
const qs = params.toString();
|
|
143
|
+
const data = await client.request('GET', qs ? `/get_expenses?${qs}` : '/get_expenses');
|
|
144
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
145
|
+
}
|
|
146
|
+
case 'sw_get_expense': {
|
|
147
|
+
const { id } = args;
|
|
148
|
+
const data = await client.request('GET', `/get_expense/${id}`);
|
|
149
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
150
|
+
}
|
|
151
|
+
case 'sw_create_expense': {
|
|
152
|
+
const body = buildExpenseBody(args);
|
|
153
|
+
const data = await client.request('POST', '/create_expense', body);
|
|
154
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
155
|
+
}
|
|
156
|
+
case 'sw_update_expense': {
|
|
157
|
+
const { expense_id } = args;
|
|
158
|
+
const body = buildExpenseBody(args);
|
|
159
|
+
const data = await client.request('POST', `/update_expense/${expense_id}`, body);
|
|
160
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
161
|
+
}
|
|
162
|
+
case 'sw_delete_expense': {
|
|
163
|
+
const { id } = args;
|
|
164
|
+
const data = await client.request('POST', `/delete_expense/${id}`);
|
|
165
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: '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
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
7
|
+
},
|
|
8
|
+
// TODO: sw_create_friend — POST /create_friend — add a friend by email (user_email, user_first_name, user_last_name)
|
|
9
|
+
// TODO: sw_delete_friend — POST /delete_friend/{id} — remove a friendship
|
|
10
|
+
];
|
|
11
|
+
export async function handleTool(name, _args, client) {
|
|
12
|
+
switch (name) {
|
|
13
|
+
case 'sw_list_friends': {
|
|
14
|
+
const data = await client.request('GET', '/get_friends');
|
|
15
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
16
|
+
}
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'sw_list_groups',
|
|
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
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'sw_get_group',
|
|
10
|
+
description: 'Get details of a single Splitwise group including all members and balances.',
|
|
11
|
+
annotations: { readOnlyHint: true },
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
id: { type: 'integer', description: 'Group ID' },
|
|
16
|
+
},
|
|
17
|
+
required: ['id'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'sw_create_group',
|
|
22
|
+
description: 'Create a new Splitwise group.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
name: { type: 'string', description: 'Group name' },
|
|
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'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'sw_add_user_to_group',
|
|
39
|
+
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
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
group_id: { type: 'integer', description: 'Group ID' },
|
|
44
|
+
user_id: { type: 'integer', description: 'User ID (preferred)' },
|
|
45
|
+
first_name: { type: 'string' },
|
|
46
|
+
last_name: { type: 'string' },
|
|
47
|
+
email: { type: 'string' },
|
|
48
|
+
},
|
|
49
|
+
required: ['group_id'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'sw_remove_user_from_group',
|
|
54
|
+
description: 'Remove a user from a Splitwise group.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
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
|
+
},
|
|
63
|
+
},
|
|
64
|
+
// TODO: sw_delete_group — POST /delete_group/{id} — soft-delete a group
|
|
65
|
+
// TODO: sw_undelete_group — POST /undelete_group/{id} — restore a soft-deleted group
|
|
66
|
+
];
|
|
67
|
+
export async function handleTool(name, args, client) {
|
|
68
|
+
switch (name) {
|
|
69
|
+
case 'sw_list_groups': {
|
|
70
|
+
const data = await client.request('GET', '/get_groups');
|
|
71
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
72
|
+
}
|
|
73
|
+
case 'sw_get_group': {
|
|
74
|
+
const { id } = args;
|
|
75
|
+
const data = await client.request('GET', `/get_group/${id}`);
|
|
76
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
77
|
+
}
|
|
78
|
+
case 'sw_create_group': {
|
|
79
|
+
const { name, group_type, simplify_by_default } = args;
|
|
80
|
+
const body = { name };
|
|
81
|
+
if (group_type !== undefined)
|
|
82
|
+
body.group_type = group_type;
|
|
83
|
+
if (simplify_by_default !== undefined)
|
|
84
|
+
body.simplify_by_default = simplify_by_default;
|
|
85
|
+
const data = await client.request('POST', '/create_group', body);
|
|
86
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
87
|
+
}
|
|
88
|
+
case 'sw_add_user_to_group': {
|
|
89
|
+
const { group_id, user_id, first_name, last_name, email } = args;
|
|
90
|
+
let body;
|
|
91
|
+
if (user_id !== undefined) {
|
|
92
|
+
body = { group_id, user_id };
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (!first_name || !last_name || !email) {
|
|
96
|
+
throw new Error('first_name, last_name, and email are required when user_id is not provided');
|
|
97
|
+
}
|
|
98
|
+
body = { group_id, first_name, last_name, email };
|
|
99
|
+
}
|
|
100
|
+
const data = await client.request('POST', '/add_user_to_group', body);
|
|
101
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
102
|
+
}
|
|
103
|
+
case 'sw_remove_user_from_group': {
|
|
104
|
+
const { group_id, user_id } = args;
|
|
105
|
+
const data = await client.request('POST', '/remove_user_from_group', { group_id, user_id });
|
|
106
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'sw_get_current_user',
|
|
4
|
+
description: 'Get the authenticated Splitwise user\'s profile (id, first_name, last_name, email). Use the returned id when building custom expense splits.',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
7
|
+
},
|
|
8
|
+
// TODO: sw_get_user — GET /get_user/{id} — get another user's profile by id
|
|
9
|
+
// TODO: sw_update_user — POST /update_user/{id} — update current user's profile (first_name, last_name, email, password, locale, default_currency)
|
|
10
|
+
];
|
|
11
|
+
export async function handleTool(name, _args, client) {
|
|
12
|
+
switch (name) {
|
|
13
|
+
case 'sw_get_current_user': {
|
|
14
|
+
const data = await client.request('GET', '/get_current_user');
|
|
15
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
16
|
+
}
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const toolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'sw_get_notifications',
|
|
4
|
+
description: 'Get recent Splitwise activity notifications for the current user.',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
6
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'sw_get_categories',
|
|
10
|
+
description: 'Get the hierarchical list of Splitwise expense categories. Use the returned id as category_id when creating expenses.',
|
|
11
|
+
annotations: { readOnlyHint: true },
|
|
12
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'sw_get_currencies',
|
|
16
|
+
description: 'Get all Splitwise-supported currency codes and units. Use the currency_code value when creating expenses in non-default currencies.',
|
|
17
|
+
annotations: { readOnlyHint: true },
|
|
18
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
19
|
+
},
|
|
20
|
+
// TODO: sw_get_comments — GET /get_comments?expense_id= — get comments on an expense
|
|
21
|
+
// TODO: sw_create_comment — POST /create_comment — add a comment to an expense (expense_id, content)
|
|
22
|
+
// TODO: sw_delete_comment — POST /delete_comment/{id} — delete a comment
|
|
23
|
+
];
|
|
24
|
+
export async function handleTool(name, _args, client) {
|
|
25
|
+
switch (name) {
|
|
26
|
+
case 'sw_get_notifications': {
|
|
27
|
+
const data = await client.request('GET', '/get_notifications');
|
|
28
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
29
|
+
}
|
|
30
|
+
case 'sw_get_categories': {
|
|
31
|
+
const data = await client.request('GET', '/get_categories');
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
33
|
+
}
|
|
34
|
+
case 'sw_get_currencies': {
|
|
35
|
+
const data = await client.request('GET', '/get_currencies');
|
|
36
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
37
|
+
}
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "splitwise-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Splitwise MCP server for Claude — developed and maintained by AI (Claude Sonnet 4.6)",
|
|
5
|
+
"author": "Claude Sonnet 4.6 (AI) <https://www.anthropic.com/claude>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"splitwise-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "node dist/index.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:coverage": "vitest run --coverage"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
20
|
+
"dotenv": "^17.3.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.5.0",
|
|
24
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vitest": "^4.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|