cleanhouse-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.
Files changed (2) hide show
  1. package/index.js +223 -0
  2. package/package.json +16 -0
package/index.js ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const url = require('url');
6
+
7
+ const BASE_URL = process.env.CLEANHOUSE_URL || 'https://web-production-65a7a.up.railway.app';
8
+ const USERNAME = process.env.CLEANHOUSE_USERNAME || '';
9
+ const PASSWORD = process.env.CLEANHOUSE_PASSWORD || '';
10
+
11
+ let sessionCookie = '';
12
+
13
+ function request(method, path, body) {
14
+ return new Promise((resolve, reject) => {
15
+ const parsed = url.parse(BASE_URL + path);
16
+ const lib = parsed.protocol === 'https:' ? https : http;
17
+ const data = body ? JSON.stringify(body) : null;
18
+ const options = {
19
+ hostname: parsed.hostname,
20
+ port: parsed.port,
21
+ path: parsed.path,
22
+ method,
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ ...(sessionCookie ? { Cookie: sessionCookie } : {}),
26
+ ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {})
27
+ }
28
+ };
29
+ const req = lib.request(options, res => {
30
+ let raw = '';
31
+ res.on('data', c => raw += c);
32
+ res.on('end', () => {
33
+ if (res.headers['set-cookie']) {
34
+ sessionCookie = res.headers['set-cookie'].map(c => c.split(';')[0]).join('; ');
35
+ }
36
+ try { resolve(JSON.parse(raw)); } catch { resolve({}); }
37
+ });
38
+ });
39
+ req.on('error', reject);
40
+ if (data) req.write(data);
41
+ req.end();
42
+ });
43
+ }
44
+
45
+ async function ensureLogin() {
46
+ if (sessionCookie) return true;
47
+ const r = await request('POST', '/api/auth/login', { username: USERNAME, password: PASSWORD });
48
+ return !!(r.ok || r.user);
49
+ }
50
+
51
+ const TOOLS = [
52
+ {
53
+ name: 'get_members',
54
+ description: 'Get all family members with points, coins and streaks',
55
+ inputSchema: { type: 'object', properties: {} }
56
+ },
57
+ {
58
+ name: 'get_tasks',
59
+ description: 'Get all tasks, optionally filtered by room name',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ room_name: { type: 'string', description: 'Filter by room name (optional)' }
64
+ }
65
+ }
66
+ },
67
+ {
68
+ name: 'add_task',
69
+ description: 'Add a new cleaning task',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ name: { type: 'string', description: 'Task name' },
74
+ room_name: { type: 'string', description: 'Room name' },
75
+ difficulty: { type: 'string', enum: ['easy', 'medium', 'hard'] },
76
+ freq: { type: 'string', enum: ['daily', 'every2', 'weekly', 'biweekly', 'monthly'] }
77
+ },
78
+ required: ['name']
79
+ }
80
+ },
81
+ {
82
+ name: 'complete_task',
83
+ description: 'Mark a task as completed by a family member',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ task_name: { type: 'string', description: 'Task name' },
88
+ member_name: { type: 'string', description: 'Member name' }
89
+ },
90
+ required: ['task_name']
91
+ }
92
+ },
93
+ {
94
+ name: 'get_leaderboard',
95
+ description: 'Get family leaderboard rankings',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ period: { type: 'string', enum: ['week', 'month', 'all'] }
100
+ }
101
+ }
102
+ },
103
+ {
104
+ name: 'add_room',
105
+ description: 'Add a new room to the household',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ name: { type: 'string', description: 'Room name' },
110
+ emoji: { type: 'string', description: 'Room emoji' }
111
+ },
112
+ required: ['name']
113
+ }
114
+ },
115
+ {
116
+ name: 'get_rooms',
117
+ description: 'Get all rooms with cleanliness percentage',
118
+ inputSchema: { type: 'object', properties: {} }
119
+ }
120
+ ];
121
+
122
+ async function handleTool(name, args) {
123
+ const ok = await ensureLogin();
124
+ if (!ok) return 'Login failed. Check CLEANHOUSE_USERNAME and CLEANHOUSE_PASSWORD.';
125
+
126
+ const data = await request('GET', '/api/data');
127
+
128
+ if (name === 'get_members') {
129
+ return (data.members || [])
130
+ .map(m => `${m.emoji} ${m.name} — ${m.points} pts | 🪙${m.coins} | 🔥${m.streak} streak`)
131
+ .join('\n') || 'No members.';
132
+ }
133
+
134
+ if (name === 'get_rooms') {
135
+ return (data.rooms || [])
136
+ .map(r => `${r.emoji} ${r.name} — ${r.cleanliness}% clean`)
137
+ .join('\n') || 'No rooms.';
138
+ }
139
+
140
+ if (name === 'get_tasks') {
141
+ const rooms = Object.fromEntries((data.rooms || []).map(r => [r.id, r.name]));
142
+ const filter = (args.room_name || '').toLowerCase();
143
+ const tasks = (data.tasks || []).filter(t => !filter || (rooms[t.room_id] || '').toLowerCase().includes(filter));
144
+ return tasks.map(t => `• ${t.name} [${rooms[t.room_id] || '?'}] — ${t.diff} — ${t.freq}`).join('\n') || 'No tasks.';
145
+ }
146
+
147
+ if (name === 'add_task') {
148
+ const rooms = data.rooms || [];
149
+ const roomName = (args.room_name || '').toLowerCase();
150
+ const room = rooms.find(r => r.name.toLowerCase().includes(roomName)) || rooms[0];
151
+ const r = await request('POST', '/api/tasks', {
152
+ name: args.name,
153
+ room_id: room ? room.id : null,
154
+ diff: args.difficulty || 'medium',
155
+ freq: args.freq || 'weekly',
156
+ approval_needed: false,
157
+ one_time: false
158
+ });
159
+ return r.ok ? `Task '${args.name}' added!` : `Error: ${r.error}`;
160
+ }
161
+
162
+ if (name === 'complete_task') {
163
+ const tasks = data.tasks || [];
164
+ const members = data.members || [];
165
+ const task = tasks.find(t => t.name.toLowerCase().includes((args.task_name || '').toLowerCase()));
166
+ if (!task) return `Task '${args.task_name}' not found.`;
167
+ const member = members.find(m => m.name.toLowerCase().includes((args.member_name || '').toLowerCase())) || members[0];
168
+ if (!member) return 'No members found.';
169
+ const r = await request('POST', `/api/tasks/${task.id}/complete`, { member_id: member.id });
170
+ return r.ok ? `✅ '${task.name}' completed by ${member.name}! +${r.pts || '?'} pts` : `Error: ${r.error}`;
171
+ }
172
+
173
+ if (name === 'get_leaderboard') {
174
+ const period = args.period || 'week';
175
+ const lb = await request('GET', `/api/leaderboard?period=${period}`);
176
+ if (!Array.isArray(lb)) return 'Could not fetch leaderboard.';
177
+ const medals = ['🥇', '🥈', '🥉'];
178
+ return lb.map((m, i) => `${medals[i] || (i+1)+'.'} ${m.emoji} ${m.name} — ${m.period_pts || 0} pts | total: ${m.points} | 🪙${m.coins}`).join('\n');
179
+ }
180
+
181
+ if (name === 'add_room') {
182
+ const r = await request('POST', '/api/rooms', { name: args.name, emoji: args.emoji || '🏠' });
183
+ return r.ok ? `Room '${args.name}' added!` : `Error: ${r.error}`;
184
+ }
185
+
186
+ return `Unknown tool: ${name}`;
187
+ }
188
+
189
+ // MCP stdio JSON-RPC
190
+ const readline = require('readline');
191
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
192
+
193
+ rl.on('line', async line => {
194
+ line = line.trim();
195
+ if (!line) return;
196
+ let msg;
197
+ try { msg = JSON.parse(line); } catch { return; }
198
+
199
+ const { id, method, params } = msg;
200
+ let resp;
201
+
202
+ if (method === 'initialize') {
203
+ resp = {
204
+ jsonrpc: '2.0', id,
205
+ result: {
206
+ protocolVersion: '2024-11-05',
207
+ capabilities: { tools: {} },
208
+ serverInfo: { name: 'cleanhouse-mcp', version: '1.0.0' }
209
+ }
210
+ };
211
+ } else if (method === 'tools/list') {
212
+ resp = { jsonrpc: '2.0', id, result: { tools: TOOLS } };
213
+ } else if (method === 'tools/call') {
214
+ const text = await handleTool(params.name, params.arguments || {});
215
+ resp = { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } };
216
+ } else if (method === 'notifications/initialized') {
217
+ return;
218
+ } else {
219
+ resp = { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
220
+ }
221
+
222
+ process.stdout.write(JSON.stringify(resp) + '\n');
223
+ });
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "cleanhouse-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for CleanHouse - household chore management app",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "cleanhouse-mcp": "index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": ["mcp", "cleanhouse", "chores", "household", "claude"],
13
+ "author": "CleanHouse",
14
+ "license": "MIT",
15
+ "dependencies": {}
16
+ }