splitwise-mcp 2.0.0 → 2.0.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.
@@ -1,142 +1,88 @@
1
- export const toolDefinitions = [
2
- {
3
- name: 'sw_list_groups',
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
- inputSchema: { type: 'object', properties: {}, required: [] },
7
- },
8
- {
9
- name: 'sw_get_group',
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
- type: 'object',
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
- name: 'sw_create_group',
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
- 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'],
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
- 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.',
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
- 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'],
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
- name: 'sw_remove_user_from_group',
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
- 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
+ group_id: z.number().describe('Group ID'),
63
+ user_id: z.number().describe('User ID to remove'),
62
64
  },
63
- },
64
- {
65
- name: 'sw_delete_group',
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
- type: 'object',
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
- name: 'sw_undelete_group',
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
- type: 'object',
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
- export async function handleTool(name, args, client) {
89
- switch (name) {
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
  }
@@ -1,70 +1,48 @@
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.',
1
+ import { z } from 'zod';
2
+ export function registerUserTools(server, client) {
3
+ server.registerTool('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
5
  annotations: { readOnlyHint: true },
6
- inputSchema: { type: 'object', properties: {}, required: [] },
7
- },
8
- {
9
- name: 'sw_get_user',
10
- description: 'Get another Splitwise user\'s profile by id.',
6
+ }, async () => {
7
+ const data = await client.request('GET', '/get_current_user');
8
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
9
+ });
10
+ server.registerTool('sw_get_user', {
11
+ description: "Get another Splitwise user's profile by id.",
11
12
  annotations: { readOnlyHint: true },
12
13
  inputSchema: {
13
- type: 'object',
14
- properties: {
15
- id: { type: 'integer', description: 'User ID' },
16
- },
17
- required: ['id'],
14
+ id: z.number().describe('User ID'),
18
15
  },
19
- },
20
- {
21
- name: 'sw_update_user',
22
- description: 'Update the current user\'s profile fields. id must be the current user\'s id.',
16
+ }, async ({ id }) => {
17
+ const data = await client.request('GET', `/get_user/${id}`);
18
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
19
+ });
20
+ server.registerTool('sw_update_user', {
21
+ description: "Update the current user's profile fields. id must be the current user's id.",
23
22
  inputSchema: {
24
- type: 'object',
25
- properties: {
26
- id: { type: 'integer', description: 'User ID (must be the current user\'s id)' },
27
- first_name: { type: 'string' },
28
- last_name: { type: 'string' },
29
- email: { type: 'string' },
30
- password: { type: 'string' },
31
- locale: { type: 'string' },
32
- default_currency: { type: 'string' },
33
- },
34
- required: ['id'],
23
+ id: z.number().describe("User ID (must be the current user's id)"),
24
+ first_name: z.string().optional(),
25
+ last_name: z.string().optional(),
26
+ email: z.string().optional(),
27
+ password: z.string().optional(),
28
+ locale: z.string().optional(),
29
+ default_currency: z.string().optional(),
35
30
  },
36
- },
37
- ];
38
- export async function handleTool(name, args, client) {
39
- switch (name) {
40
- case 'sw_get_current_user': {
41
- const data = await client.request('GET', '/get_current_user');
42
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
43
- }
44
- case 'sw_get_user': {
45
- const { id } = args;
46
- const data = await client.request('GET', `/get_user/${id}`);
47
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
48
- }
49
- case 'sw_update_user': {
50
- const { id, first_name, last_name, email, password, locale, default_currency } = args;
51
- const body = {};
52
- if (first_name !== undefined)
53
- body.first_name = first_name;
54
- if (last_name !== undefined)
55
- body.last_name = last_name;
56
- if (email !== undefined)
57
- body.email = email;
58
- if (password !== undefined)
59
- body.password = password;
60
- if (locale !== undefined)
61
- body.locale = locale;
62
- if (default_currency !== undefined)
63
- body.default_currency = default_currency;
64
- const data = await client.request('POST', `/update_user/${id}`, body);
65
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
66
- }
67
- default:
68
- throw new Error(`Unknown tool: ${name}`);
69
- }
31
+ }, async ({ id, first_name, last_name, email, password, locale, default_currency }) => {
32
+ const body = {};
33
+ if (first_name !== undefined)
34
+ body.first_name = first_name;
35
+ if (last_name !== undefined)
36
+ body.last_name = last_name;
37
+ if (email !== undefined)
38
+ body.email = email;
39
+ if (password !== undefined)
40
+ body.password = password;
41
+ if (locale !== undefined)
42
+ body.locale = locale;
43
+ if (default_currency !== undefined)
44
+ body.default_currency = default_currency;
45
+ const data = await client.request('POST', `/update_user/${id}`, body);
46
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
47
+ });
70
48
  }
@@ -1,89 +1,54 @@
1
- export const toolDefinitions = [
2
- {
3
- name: 'sw_get_notifications',
1
+ import { z } from 'zod';
2
+ export function registerUtilityTools(server, client) {
3
+ server.registerTool('sw_get_notifications', {
4
4
  description: 'Get recent Splitwise activity notifications for the current user.',
5
5
  annotations: { readOnlyHint: true },
6
- inputSchema: { type: 'object', properties: {}, required: [] },
7
- },
8
- {
9
- name: 'sw_get_categories',
6
+ }, async () => {
7
+ const data = await client.request('GET', '/get_notifications');
8
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
9
+ });
10
+ server.registerTool('sw_get_categories', {
10
11
  description: 'Get the hierarchical list of Splitwise expense categories. Use the returned id as category_id when creating expenses.',
11
12
  annotations: { readOnlyHint: true },
12
- inputSchema: { type: 'object', properties: {}, required: [] },
13
- },
14
- {
15
- name: 'sw_get_currencies',
13
+ }, async () => {
14
+ const data = await client.request('GET', '/get_categories');
15
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
16
+ });
17
+ server.registerTool('sw_get_currencies', {
16
18
  description: 'Get all Splitwise-supported currency codes and units. Use the currency_code value when creating expenses in non-default currencies.',
17
19
  annotations: { readOnlyHint: true },
18
- inputSchema: { type: 'object', properties: {}, required: [] },
19
- },
20
- {
21
- name: 'sw_get_comments',
20
+ }, async () => {
21
+ const data = await client.request('GET', '/get_currencies');
22
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
23
+ });
24
+ server.registerTool('sw_get_comments', {
22
25
  description: 'Get all comments on a Splitwise expense.',
23
26
  annotations: { readOnlyHint: true },
24
27
  inputSchema: {
25
- type: 'object',
26
- properties: {
27
- expense_id: { type: 'integer', description: 'Expense ID to get comments for' },
28
- },
29
- required: ['expense_id'],
28
+ expense_id: z.number().describe('Expense ID to get comments for'),
30
29
  },
31
- },
32
- {
33
- name: 'sw_create_comment',
30
+ }, async ({ expense_id }) => {
31
+ const data = await client.request('GET', `/get_comments?expense_id=${expense_id}`);
32
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
33
+ });
34
+ server.registerTool('sw_create_comment', {
34
35
  description: 'Add a comment to a Splitwise expense.',
35
36
  inputSchema: {
36
- type: 'object',
37
- properties: {
38
- expense_id: { type: 'integer', description: 'Expense ID to comment on' },
39
- content: { type: 'string', description: 'Comment text' },
40
- },
41
- required: ['expense_id', 'content'],
37
+ expense_id: z.number().describe('Expense ID to comment on'),
38
+ content: z.string().describe('Comment text'),
42
39
  },
43
- },
44
- {
45
- name: 'sw_delete_comment',
40
+ }, async ({ expense_id, content }) => {
41
+ const data = await client.request('POST', '/create_comment', { expense_id, content });
42
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
43
+ });
44
+ server.registerTool('sw_delete_comment', {
46
45
  description: 'Delete a comment by id.',
47
46
  annotations: { destructiveHint: true },
48
47
  inputSchema: {
49
- type: 'object',
50
- properties: {
51
- id: { type: 'integer', description: 'Comment ID to delete' },
52
- },
53
- required: ['id'],
48
+ id: z.number().describe('Comment ID to delete'),
54
49
  },
55
- },
56
- ];
57
- export async function handleTool(name, args, client) {
58
- switch (name) {
59
- case 'sw_get_notifications': {
60
- const data = await client.request('GET', '/get_notifications');
61
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
62
- }
63
- case 'sw_get_categories': {
64
- const data = await client.request('GET', '/get_categories');
65
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
66
- }
67
- case 'sw_get_currencies': {
68
- const data = await client.request('GET', '/get_currencies');
69
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
70
- }
71
- case 'sw_get_comments': {
72
- const { expense_id } = args;
73
- const data = await client.request('GET', `/get_comments?expense_id=${expense_id}`);
74
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
75
- }
76
- case 'sw_create_comment': {
77
- const { expense_id, content } = args;
78
- const data = await client.request('POST', '/create_comment', { expense_id, content });
79
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
80
- }
81
- case 'sw_delete_comment': {
82
- const { id } = args;
83
- const data = await client.request('POST', `/delete_comment/${id}`);
84
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
85
- }
86
- default:
87
- throw new Error(`Unknown tool: ${name}`);
88
- }
50
+ }, async ({ id }) => {
51
+ const data = await client.request('POST', `/delete_comment/${id}`);
52
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
53
+ });
89
54
  }
package/package.json CHANGED
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "name": "splitwise-mcp",
3
- "version": "2.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>",
3
+ "version": "2.0.2",
4
+ "mcpName": "io.github.chrischall/splitwise-mcp",
5
+ "description": "Splitwise MCP server for Claude — developed and maintained by AI (Claude Code)",
6
+ "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/chrischall/splitwise-mcp.git"
9
10
  },
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "mcp",
14
+ "model-context-protocol",
15
+ "claude",
16
+ "ai",
17
+ "splitwise",
18
+ "expenses",
19
+ "expense-tracking",
20
+ "groups",
21
+ "friends",
22
+ "split-bills",
23
+ "finance",
24
+ "shared-expenses"
25
+ ],
10
26
  "type": "module",
11
27
  "bin": {
12
28
  "splitwise-mcp": "dist/index.js"
@@ -15,7 +31,8 @@
15
31
  "dist",
16
32
  ".claude-plugin",
17
33
  "SKILL.md",
18
- ".mcp.json"
34
+ ".mcp.json",
35
+ "server.json"
19
36
  ],
20
37
  "scripts": {
21
38
  "build": "tsc && npm run bundle",
@@ -27,12 +44,13 @@
27
44
  },
28
45
  "dependencies": {
29
46
  "@modelcontextprotocol/sdk": "^1.29.0",
30
- "dotenv": "^17.4.0"
47
+ "dotenv": "^17.4.0",
48
+ "zod": "^4.3.6"
31
49
  },
32
50
  "devDependencies": {
33
51
  "@types/node": "^25.5.2",
34
52
  "@vitest/coverage-v8": "^4.1.2",
35
- "esbuild": "^0.27.0",
53
+ "esbuild": "^0.28.0",
36
54
  "typescript": "^6.0.2",
37
55
  "vitest": "^4.1.2"
38
56
  }
package/server.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chrischall/splitwise-mcp",
4
+ "description": "Splitwise expense and group management for Claude — shared expenses, settle up, friends",
5
+ "repository": {
6
+ "url": "https://github.com/chrischall/splitwise-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "2.0.2",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "splitwise-mcp",
14
+ "version": "2.0.2",
15
+ "transport": {
16
+ "type": "stdio"
17
+ },
18
+ "environmentVariables": [
19
+ {
20
+ "name": "SPLITWISE_API_KEY",
21
+ "description": "Your Splitwise API key (splitwise.com/apps/api)",
22
+ "isRequired": true,
23
+ "format": "string",
24
+ "isSecret": true
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ }