Reubencf commited on
Commit
c3ae75f
·
1 Parent(s): e099ac1

feat: Update MCP integration to use passkey system instead of sessionId

Browse files
Files changed (3) hide show
  1. CLAUDE_INTEGRATION_GUIDE.md +362 -0
  2. mcp-server.js +219 -246
  3. pages/api/mcp-handler.js +116 -87
CLAUDE_INTEGRATION_GUIDE.md ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Claude Desktop Integration Guide - Passkey System
2
+
3
+ ## Overview
4
+ Claude Desktop can now generate content and upload files directly to your ReubenOS on Hugging Face using the **passkey system** for secure storage!
5
+
6
+ ## 🔐 How It Works
7
+
8
+ ### **Two Storage Options:**
9
+
10
+ 1. **🔐 Secure Data (with Passkey)**
11
+ - Files are private and isolated by passkey
12
+ - Each passkey creates a separate secure folder
13
+ - Perfect for personal documents, code, quizzes
14
+
15
+ 2. **📢 Public Files**
16
+ - Files are publicly accessible to everyone
17
+ - No passkey required
18
+ - Great for sharing content
19
+
20
+ ## 🚀 Available MCP Tools
21
+
22
+ ### 1. **save_file**
23
+ Save any file to ReubenOS (code, documents, data, etc.)
24
+
25
+ **Examples:**
26
+
27
+ ```javascript
28
+ // Save a Flutter app (secure)
29
+ {
30
+ "fileName": "main.dart",
31
+ "content": "import 'package:flutter/material.dart'...",
32
+ "passkey": "my-secret-key"
33
+ }
34
+
35
+ // Save a public document
36
+ {
37
+ "fileName": "tutorial.md",
38
+ "content": "# Flutter Tutorial\n...",
39
+ "isPublic": true
40
+ }
41
+
42
+ // Save a LaTeX document
43
+ {
44
+ "fileName": "report.tex",
45
+ "content": "\\documentclass{article}...",
46
+ "passkey": "my-secret-key"
47
+ }
48
+ ```
49
+
50
+ ### 2. **list_files**
51
+ List all files in your storage
52
+
53
+ **Examples:**
54
+
55
+ ```javascript
56
+ // List your secure files
57
+ {
58
+ "passkey": "my-secret-key"
59
+ }
60
+
61
+ // List public files
62
+ {
63
+ "isPublic": true
64
+ }
65
+ ```
66
+
67
+ ### 3. **delete_file**
68
+ Delete a specific file
69
+
70
+ **Examples:**
71
+
72
+ ```javascript
73
+ // Delete from secure storage
74
+ {
75
+ "fileName": "old_file.dart",
76
+ "passkey": "my-secret-key"
77
+ }
78
+
79
+ // Delete from public folder
80
+ {
81
+ "fileName": "old_doc.md",
82
+ "isPublic": true
83
+ }
84
+ ```
85
+
86
+ ### 4. **deploy_quiz**
87
+ Deploy an interactive quiz to ReubenOS
88
+
89
+ **Example:**
90
+
91
+ ```javascript
92
+ {
93
+ "passkey": "my-secret-key",
94
+ "quizData": {
95
+ "title": "Flutter Basics Quiz",
96
+ "description": "Test your Flutter knowledge",
97
+ "timeLimit": 15,
98
+ "questions": [
99
+ {
100
+ "id": "q1",
101
+ "question": "What is a StatelessWidget?",
102
+ "type": "multiple-choice",
103
+ "options": [
104
+ "A widget that never changes",
105
+ "A widget with state",
106
+ "A button widget",
107
+ "A layout widget"
108
+ ],
109
+ "correctAnswer": 0,
110
+ "explanation": "StatelessWidget is immutable",
111
+ "points": 1
112
+ },
113
+ {
114
+ "id": "q2",
115
+ "question": "Flutter uses Dart language",
116
+ "type": "true-false",
117
+ "correctAnswer": true,
118
+ "points": 1
119
+ }
120
+ ]
121
+ }
122
+ }
123
+ ```
124
+
125
+ ## 💬 How Claude Can Use This
126
+
127
+ ### **Conversation Examples:**
128
+
129
+ **User:** "Create a Flutter counter app and save it to my ReubenOS"
130
+
131
+ **Claude Response:**
132
+ ✅ I'll create a Flutter counter app and save it using your passkey.
133
+
134
+ ```dart
135
+ import 'package:flutter/material.dart';
136
+
137
+ void main() => runApp(CounterApp());
138
+
139
+ class CounterApp extends StatefulWidget {
140
+ @override
141
+ _CounterAppState createState() => _CounterAppState();
142
+ }
143
+
144
+ class _CounterAppState extends State<CounterApp> {
145
+ int _counter = 0;
146
+
147
+ void _incrementCounter() {
148
+ setState(() {
149
+ _counter++;
150
+ });
151
+ }
152
+
153
+ @override
154
+ Widget build(BuildContext context) {
155
+ return MaterialApp(...);
156
+ }
157
+ }
158
+ ```
159
+
160
+ *[Uses MCP save_file tool]*
161
+
162
+ ✅ File saved successfully!
163
+ 📄 File: counter_app.dart
164
+ 📍 Location: 🔐 Secure Data (Key: your-passkey)
165
+
166
+ ---
167
+
168
+ **User:** "Create a quiz about Python and deploy it"
169
+
170
+ **Claude Response:**
171
+ ✅ I'll create a Python quiz and deploy it to your ReubenOS.
172
+
173
+ *[Uses MCP deploy_quiz tool]*
174
+
175
+ ✅ Quiz deployed successfully!
176
+ 📝 Quiz: Python Fundamentals
177
+ 📊 Questions: 5
178
+ ⏱️ Time Limit: 10 min
179
+ 🎯 Total Points: 5
180
+ 📍 Location: 🔐 Secure (Key: your-passkey)
181
+
182
+ 🚀 Quiz is now available in Reuben OS Quiz App!
183
+
184
+ ---
185
+
186
+ **User:** "Save this tutorial as a public file so others can see it"
187
+
188
+ **Claude Response:**
189
+ ✅ I'll save this tutorial to the public folder.
190
+
191
+ *[Uses MCP save_file with isPublic=true]*
192
+
193
+ ✅ File saved successfully!
194
+ 📄 File: flutter_tutorial.md
195
+ 📍 Location: 📢 Public Folder
196
+ 🌐 URL: https://mcp-1st-birthday-reuben-os.hf.space/data/public/flutter_tutorial.md
197
+
198
+ ## 🔑 Passkey Best Practices
199
+
200
+ 1. **Choose a Memorable Passkey**
201
+ - Use alphanumeric characters, hyphens, or underscores
202
+ - Minimum 4 characters
203
+ - Example: `my-flutter-projects`, `john-doe-123`
204
+
205
+ 2. **Organize by Project**
206
+ - Use different passkeys for different projects
207
+ - Example: `flutter-apps`, `latex-docs`, `quiz-bank`
208
+
209
+ 3. **Public vs Private**
210
+ - Use **public** for tutorials, examples, shared content
211
+ - Use **passkey** for personal work, drafts, private quizzes
212
+
213
+ ## 📂 Accessing Files in ReubenOS
214
+
215
+ ### **Via Web Interface:**
216
+
217
+ 1. Open https://mcp-1st-birthday-reuben-os.hf.space
218
+ 2. Click "File Manager" in the dock
219
+ 3. For **Secure Data**:
220
+ - Click "Secure Data" in sidebar
221
+ - Enter your passkey
222
+ - Your files appear!
223
+ 4. For **Public Files**:
224
+ - Click "Public Files" in sidebar
225
+ - All public files are visible
226
+
227
+ ### **Via Claude Desktop:**
228
+
229
+ Use the `list_files` tool to see what's stored:
230
+
231
+ ```javascript
232
+ // Your secure files
233
+ { "passkey": "your-passkey" }
234
+
235
+ // Public files
236
+ { "isPublic": true }
237
+ ```
238
+
239
+ ## 🎯 Common Use Cases
240
+
241
+ ### **1. Flutter Development**
242
+ ```javascript
243
+ // Save main.dart
244
+ save_file({
245
+ fileName: "main.dart",
246
+ content: "...",
247
+ passkey: "flutter-project-1"
248
+ })
249
+
250
+ // Save widget file
251
+ save_file({
252
+ fileName: "custom_widget.dart",
253
+ content: "...",
254
+ passkey: "flutter-project-1"
255
+ })
256
+ ```
257
+
258
+ ### **2. LaTeX Documents**
259
+ ```javascript
260
+ save_file({
261
+ fileName: "thesis.tex",
262
+ content: "\\documentclass{article}...",
263
+ passkey: "academic-work"
264
+ })
265
+ ```
266
+
267
+ ### **3. Quiz Creation**
268
+ ```javascript
269
+ deploy_quiz({
270
+ passkey: "teaching-materials",
271
+ quizData: {
272
+ title: "Week 1 Quiz",
273
+ questions: [...]
274
+ }
275
+ })
276
+ ```
277
+
278
+ ### **4. Public Tutorials**
279
+ ```javascript
280
+ save_file({
281
+ fileName: "react-guide.md",
282
+ content: "# React Guide...",
283
+ isPublic: true
284
+ })
285
+ ```
286
+
287
+ ## 🔄 Workflow Example
288
+
289
+ **Scenario:** Create a complete Flutter project
290
+
291
+ 1. **Claude creates main.dart:**
292
+ ```javascript
293
+ save_file({
294
+ fileName: "main.dart",
295
+ content: "...",
296
+ passkey: "my-flutter-app"
297
+ })
298
+ ```
299
+
300
+ 2. **Claude creates README:**
301
+ ```javascript
302
+ save_file({
303
+ fileName: "README.md",
304
+ content: "# My Flutter App...",
305
+ passkey: "my-flutter-app"
306
+ })
307
+ ```
308
+
309
+ 3. **Claude creates a quiz for testing:**
310
+ ```javascript
311
+ deploy_quiz({
312
+ passkey: "my-flutter-app",
313
+ quizData: {
314
+ title: "Flutter App Quiz",
315
+ questions: [...]
316
+ }
317
+ })
318
+ ```
319
+
320
+ 4. **User opens ReubenOS:**
321
+ - Opens File Manager → Secure Data
322
+ - Enters passkey: `my-flutter-app`
323
+ - Sees all 3 files!
324
+ - Opens Quiz App to take the quiz
325
+
326
+ ## 🌐 Direct URLs
327
+
328
+ Files are accessible via:
329
+ - **Public:** `https://mcp-1st-birthday-reuben-os.hf.space/data/public/{filename}`
330
+ - **Secure:** Requires passkey authentication through the web interface
331
+
332
+ ## ⚙️ Configuration
333
+
334
+ The MCP server is configured in:
335
+ `~/Library/Application Support/Claude/claude_desktop_config.json`
336
+
337
+ ```json
338
+ {
339
+ "mcpServers": {
340
+ "reubenos": {
341
+ "command": "node",
342
+ "args": ["/path/to/mcp-server.js"],
343
+ "env": {
344
+ "REUBENOS_URL": "https://mcp-1st-birthday-reuben-os.hf.space"
345
+ }
346
+ }
347
+ }
348
+ }
349
+ ```
350
+
351
+ ## 🎉 Summary
352
+
353
+ Claude can now:
354
+ - ✅ Generate Flutter/Dart code and save it
355
+ - ✅ Create LaTeX documents
356
+ - ✅ Deploy interactive quizzes
357
+ - ✅ Save any text-based files
358
+ - ✅ Use passkeys for private storage
359
+ - ✅ Share files publicly
360
+ - ✅ List and manage files
361
+
362
+ All files are **accessible immediately** in your ReubenOS web interface on Hugging Face! 🚀
mcp-server.js CHANGED
@@ -1,5 +1,5 @@
1
  #!/usr/bin/env node
2
- // mcp-server.js - Simplified MCP Server for Reuben OS with Stateless Prefix Strategy
3
 
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -17,8 +17,8 @@ class ReubenOSMCPServer {
17
  this.server = new Server(
18
  {
19
  name: 'reubenos-mcp-server',
20
- version: '2.0.0',
21
- description: 'Simplified MCP Server for Reuben OS with stateless prefix strategy',
22
  icon: '🖥️',
23
  },
24
  {
@@ -42,94 +42,110 @@ class ReubenOSMCPServer {
42
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
43
  tools: [
44
  {
45
- name: 'manage_files',
46
- description: 'Manage files in Reuben OS - save Flutter/Dart, LaTeX, text, or any code files. Requires sessionId.',
47
  inputSchema: {
48
  type: 'object',
49
  properties: {
50
- sessionId: {
51
  type: 'string',
52
- description: 'Session ID from Reuben OS (required) - get this from the UI',
53
  },
54
- action: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  type: 'string',
56
- enum: ['save', 'retrieve', 'delete', 'clear', 'save_public'],
57
- description: 'Action to perform (use save_public to save to public folder)',
58
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  fileName: {
60
  type: 'string',
61
- description: 'File name (required for save/delete). Examples: main.dart, document.tex, script.js',
62
  },
63
- content: {
64
  type: 'string',
65
- description: 'File content (required for save action)',
 
 
 
 
66
  },
67
  },
68
- required: ['sessionId', 'action'],
69
  },
70
  },
71
  {
72
  name: 'deploy_quiz',
73
- description: 'Deploy an interactive quiz to Reuben OS. The quiz will be shown in the Quiz App.',
74
  inputSchema: {
75
  type: 'object',
76
  properties: {
77
- sessionId: {
78
  type: 'string',
79
- description: 'Session ID from Reuben OS (required) - get this from the UI',
 
 
 
 
80
  },
81
  quizData: {
82
  type: 'object',
83
- description: 'Quiz data object',
84
  properties: {
85
- title: {
86
- type: 'string',
87
- description: 'Quiz title',
88
- },
89
- description: {
90
- type: 'string',
91
- description: 'Quiz description',
92
- },
93
- timeLimit: {
94
- type: 'number',
95
- description: 'Time limit in minutes (optional)',
96
- },
97
  questions: {
98
  type: 'array',
99
- description: 'Array of quiz questions',
100
  items: {
101
  type: 'object',
102
  properties: {
103
- id: {
104
- type: 'string',
105
- description: 'Unique question ID',
106
- },
107
- question: {
108
- type: 'string',
109
- description: 'The question text',
110
- },
111
  type: {
112
  type: 'string',
113
- enum: ['multiple-choice', 'true-false', 'short-answer'],
114
- description: 'Question type',
115
  },
116
  options: {
117
  type: 'array',
118
- description: 'Answer options (for multiple-choice)',
119
- items: { type: 'string' },
120
- },
121
- correctAnswer: {
122
- type: ['string', 'number', 'boolean'],
123
- description: 'Correct answer or index',
124
- },
125
- explanation: {
126
- type: 'string',
127
- description: 'Explanation (optional)',
128
- },
129
- points: {
130
- type: 'number',
131
- description: 'Points (default: 1)',
132
  },
 
 
 
133
  },
134
  required: ['id', 'question', 'type', 'correctAnswer'],
135
  },
@@ -138,7 +154,7 @@ class ReubenOSMCPServer {
138
  required: ['title', 'questions'],
139
  },
140
  },
141
- required: ['sessionId', 'quizData'],
142
  },
143
  },
144
  ],
@@ -149,8 +165,12 @@ class ReubenOSMCPServer {
149
 
150
  try {
151
  switch (name) {
152
- case 'manage_files':
153
- return await this.manageFiles(args);
 
 
 
 
154
  case 'deploy_quiz':
155
  return await this.deployQuiz(args);
156
  default:
@@ -169,262 +189,215 @@ class ReubenOSMCPServer {
169
  });
170
  }
171
 
172
- async manageFiles(args) {
173
  try {
174
- // Validate inputs based on action
175
- if (args.action === 'save' && (!args.fileName || args.content === undefined)) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  return {
177
  content: [
178
  {
179
  type: 'text',
180
- text: '❌ Save action requires fileName and content',
181
  },
182
  ],
183
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  }
185
 
186
- if (args.action === 'delete' && !args.fileName) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  return {
188
  content: [
189
  {
190
  type: 'text',
191
- text: '❌ Delete action requires fileName',
192
  },
193
  ],
194
  };
 
 
 
 
195
  }
 
 
 
 
 
 
196
 
197
- // Handle different actions
198
- switch (args.action) {
199
- case 'save':
200
- case 'save_public': {
201
- const response = await fetch(API_ENDPOINT, {
202
- method: 'POST',
203
- headers: { 'Content-Type': 'application/json' },
204
- body: JSON.stringify({
205
- sessionId: args.sessionId,
206
- action: args.action === 'save_public' ? 'save_public' : 'save_file',
207
- fileName: args.fileName,
208
- content: args.content,
209
- }),
210
- });
211
-
212
- const data = await response.json();
213
-
214
- if (response.ok && data.success) {
215
- const location = args.action === 'save_public' ? '📢 Public Folder' : `🔑 Session: ${args.sessionId}`;
216
- return {
217
- content: [
218
- {
219
- type: 'text',
220
- text: `✅ File saved successfully!\n\n📄 File: ${args.fileName}\n${location}\n\n✨ File is now available in Reuben OS${args.action === 'save_public' ? ' (publicly accessible)' : ''}`,
221
- },
222
- ],
223
- };
224
- } else {
225
- return {
226
- content: [
227
- {
228
- type: 'text',
229
- text: `❌ Failed to save file: ${data.error || 'Unknown error'}`,
230
- },
231
- ],
232
- };
233
- }
234
- }
235
-
236
- case 'retrieve': {
237
- const response = await fetch(`${API_ENDPOINT}?sessionId=${args.sessionId}`, {
238
- method: 'GET',
239
- headers: { 'Content-Type': 'application/json' },
240
- });
241
-
242
- const data = await response.json();
243
 
244
- if (response.ok && data.success) {
245
- const fileList = data.files
246
- .map(f => `📄 ${f.name} (${(f.size / 1024).toFixed(2)} KB)${f.isQuiz ? ' [QUIZ]' : ''}`)
247
- .join('\n');
 
248
 
249
- return {
250
- content: [
251
- {
252
- type: 'text',
253
- text: `📁 Files for session ${args.sessionId}:\n\n${fileList || 'No files found'}\n\nTotal: ${data.count} file(s)`,
254
- },
255
- ],
256
- };
257
- } else {
258
- return {
259
- content: [
260
- {
261
- type: 'text',
262
- text: `❌ Failed to retrieve files: ${data.error || 'Unknown error'}`,
263
- },
264
- ],
265
- };
266
- }
267
- }
268
 
269
- case 'delete': {
270
- const response = await fetch(API_ENDPOINT, {
271
- method: 'POST',
272
- headers: { 'Content-Type': 'application/json' },
273
- body: JSON.stringify({
274
- sessionId: args.sessionId,
275
- action: 'delete_file',
276
- fileName: args.fileName,
277
- content: '',
278
- }),
279
- });
280
-
281
- const data = await response.json();
282
-
283
- if (response.ok && data.success) {
284
- return {
285
- content: [
286
- {
287
- type: 'text',
288
- text: `✅ File '${args.fileName}' deleted successfully from session ${args.sessionId}`,
289
- },
290
- ],
291
- };
292
- } else {
293
- return {
294
- content: [
295
- {
296
- type: 'text',
297
- text: `❌ Failed to delete file: ${data.error || 'File not found'}`,
298
- },
299
- ],
300
- };
301
- }
302
- }
303
 
304
- case 'clear': {
305
- const response = await fetch(API_ENDPOINT, {
306
- method: 'POST',
307
- headers: { 'Content-Type': 'application/json' },
308
- body: JSON.stringify({
309
- sessionId: args.sessionId,
310
- action: 'clear_session',
311
- fileName: '',
312
- content: '',
313
- }),
314
- });
315
-
316
- const data = await response.json();
317
-
318
- if (response.ok && data.success) {
319
- return {
320
- content: [
321
- {
322
- type: 'text',
323
- text: `✅ All files cleared for session ${args.sessionId}\n\n🗑️ Deleted ${data.deletedCount || 0} file(s)`,
324
- },
325
- ],
326
- };
327
- } else {
328
- return {
329
- content: [
330
- {
331
- type: 'text',
332
- text: `❌ Failed to clear session: ${data.error || 'Unknown error'}`,
333
- },
334
- ],
335
- };
336
- }
337
- }
338
 
339
- default:
340
- return {
341
- content: [
342
- {
343
- type: 'text',
344
- text: `❌ Unknown action: ${args.action}`,
345
- },
346
- ],
347
- };
348
  }
349
  } catch (error) {
350
- console.error('manage_files error:', error);
351
  return {
352
- content: [
353
- {
354
- type: 'text',
355
- text: `❌ Error: ${error.message}`,
356
- },
357
- ],
358
  };
359
  }
360
  }
361
 
362
  async deployQuiz(args) {
363
  try {
364
- // Validate quiz data
365
- if (!args.quizData.questions || args.quizData.questions.length === 0) {
 
366
  return {
367
- content: [
368
- {
369
- type: 'text',
370
- text: '❌ Quiz must have at least one question',
371
- },
372
- ],
 
373
  };
374
  }
375
 
376
- // Add metadata to quiz
377
  const fullQuizData = {
378
- ...args.quizData,
379
  createdAt: new Date().toISOString(),
380
- sessionId: args.sessionId,
381
  version: '1.0',
382
  };
383
 
384
- // Save quiz as quiz.json
385
  const response = await fetch(API_ENDPOINT, {
386
  method: 'POST',
387
  headers: { 'Content-Type': 'application/json' },
388
  body: JSON.stringify({
389
- sessionId: args.sessionId,
390
  action: 'deploy_quiz',
391
  fileName: 'quiz.json',
392
  content: JSON.stringify(fullQuizData, null, 2),
 
393
  }),
394
  });
395
 
396
  const data = await response.json();
397
 
398
  if (response.ok && data.success) {
399
- const totalPoints = args.quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0);
 
400
 
401
  return {
402
  content: [
403
  {
404
  type: 'text',
405
- text: `✅ Quiz deployed successfully!\n\n📝 Quiz: ${args.quizData.title}\n📊 Questions: ${args.quizData.questions.length}\n⏱️ Time Limit: ${args.quizData.timeLimit || 'No limit'} minutes\n🎯 Total Points: ${totalPoints}\n🔑 Session: ${args.sessionId}\n\n🚀 The quiz is now available in Reuben OS Quiz App!`,
406
  },
407
  ],
408
  };
409
  } else {
410
  return {
411
- content: [
412
- {
413
- type: 'text',
414
- text: `❌ Failed to deploy quiz: ${data.error || 'Unknown error'}`,
415
- },
416
- ],
417
  };
418
  }
419
  } catch (error) {
420
- console.error('deploy_quiz error:', error);
421
  return {
422
- content: [
423
- {
424
- type: 'text',
425
- text: `❌ Error deploying quiz: ${error.message}`,
426
- },
427
- ],
428
  };
429
  }
430
  }
@@ -432,7 +405,7 @@ class ReubenOSMCPServer {
432
  async run() {
433
  const transport = new StdioServerTransport();
434
  await this.server.connect(transport);
435
- console.error('ReubenOS MCP Server running...');
436
  }
437
  }
438
 
 
1
  #!/usr/bin/env node
2
+ // mcp-server.js - MCP Server for Reuben OS with Passkey System
3
 
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
 
17
  this.server = new Server(
18
  {
19
  name: 'reubenos-mcp-server',
20
+ version: '3.0.0',
21
+ description: 'MCP Server for Reuben OS with secure passkey-based file storage',
22
  icon: '🖥️',
23
  },
24
  {
 
42
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
43
  tools: [
44
  {
45
+ name: 'save_file',
46
+ description: 'Save a file to Reuben OS. Use your passkey for secure storage or save to public folder.',
47
  inputSchema: {
48
  type: 'object',
49
  properties: {
50
+ fileName: {
51
  type: 'string',
52
+ description: 'File name (e.g., main.dart, document.tex, report.pdf, quiz.json)',
53
  },
54
+ content: {
55
+ type: 'string',
56
+ description: 'File content to save',
57
+ },
58
+ passkey: {
59
+ type: 'string',
60
+ description: 'Your passkey for secure storage (min 4 characters). Leave empty to save to public folder.',
61
+ },
62
+ isPublic: {
63
+ type: 'boolean',
64
+ description: 'Set to true to save to public folder (accessible to everyone). Default: false',
65
+ },
66
+ },
67
+ required: ['fileName', 'content'],
68
+ },
69
+ },
70
+ {
71
+ name: 'list_files',
72
+ description: 'List all files in your secure storage or public folder',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ passkey: {
77
  type: 'string',
78
+ description: 'Your passkey to list secure files. Leave empty to list public files.',
 
79
  },
80
+ isPublic: {
81
+ type: 'boolean',
82
+ description: 'Set to true to list public files. Default: false',
83
+ },
84
+ },
85
+ },
86
+ },
87
+ {
88
+ name: 'delete_file',
89
+ description: 'Delete a specific file from your storage',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
  fileName: {
94
  type: 'string',
95
+ description: 'Name of the file to delete',
96
  },
97
+ passkey: {
98
  type: 'string',
99
+ description: 'Your passkey (required for secure files)',
100
+ },
101
+ isPublic: {
102
+ type: 'boolean',
103
+ description: 'Set to true if deleting from public folder. Default: false',
104
  },
105
  },
106
+ required: ['fileName'],
107
  },
108
  },
109
  {
110
  name: 'deploy_quiz',
111
+ description: 'Deploy an interactive quiz to Reuben OS Quiz App',
112
  inputSchema: {
113
  type: 'object',
114
  properties: {
115
+ passkey: {
116
  type: 'string',
117
+ description: 'Your passkey for secure quiz storage',
118
+ },
119
+ isPublic: {
120
+ type: 'boolean',
121
+ description: 'Make quiz public (default: false)',
122
  },
123
  quizData: {
124
  type: 'object',
125
+ description: 'Quiz configuration',
126
  properties: {
127
+ title: { type: 'string', description: 'Quiz title' },
128
+ description: { type: 'string', description: 'Quiz description' },
129
+ timeLimit: { type: 'number', description: 'Time limit in minutes (optional)' },
 
 
 
 
 
 
 
 
 
130
  questions: {
131
  type: 'array',
132
+ description: 'Array of questions',
133
  items: {
134
  type: 'object',
135
  properties: {
136
+ id: { type: 'string' },
137
+ question: { type: 'string' },
 
 
 
 
 
 
138
  type: {
139
  type: 'string',
140
+ enum: ['multiple-choice', 'true-false', 'short-answer']
 
141
  },
142
  options: {
143
  type: 'array',
144
+ items: { type: 'string' }
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  },
146
+ correctAnswer: { type: ['string', 'number', 'boolean'] },
147
+ explanation: { type: 'string' },
148
+ points: { type: 'number' },
149
  },
150
  required: ['id', 'question', 'type', 'correctAnswer'],
151
  },
 
154
  required: ['title', 'questions'],
155
  },
156
  },
157
+ required: ['quizData'],
158
  },
159
  },
160
  ],
 
165
 
166
  try {
167
  switch (name) {
168
+ case 'save_file':
169
+ return await this.saveFile(args);
170
+ case 'list_files':
171
+ return await this.listFiles(args);
172
+ case 'delete_file':
173
+ return await this.deleteFile(args);
174
  case 'deploy_quiz':
175
  return await this.deployQuiz(args);
176
  default:
 
189
  });
190
  }
191
 
192
+ async saveFile(args) {
193
  try {
194
+ const { fileName, content, passkey, isPublic = false } = args;
195
+
196
+ if (!fileName || content === undefined) {
197
+ return {
198
+ content: [{ type: 'text', text: '❌ fileName and content are required' }],
199
+ };
200
+ }
201
+
202
+ if (!isPublic && !passkey) {
203
+ return {
204
+ content: [{ type: 'text', text: '❌ Passkey is required for secure storage (or set isPublic=true)' }],
205
+ };
206
+ }
207
+
208
+ const response = await fetch(API_ENDPOINT, {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify({
212
+ passkey,
213
+ action: 'save_file',
214
+ fileName,
215
+ content,
216
+ isPublic,
217
+ }),
218
+ });
219
+
220
+ const data = await response.json();
221
+
222
+ if (response.ok && data.success) {
223
+ const location = isPublic ? '📢 Public Folder' : `🔐 Secure Data (Key: ${passkey})`;
224
+ const url = data.url ? `\n🌐 URL: ${data.url}` : '';
225
+
226
  return {
227
  content: [
228
  {
229
  type: 'text',
230
+ text: `✅ File saved successfully!\n\n📄 File: ${fileName}\n📍 Location: ${location}${url}\n\n✨ File is now available in Reuben OS!`,
231
  },
232
  ],
233
  };
234
+ } else {
235
+ return {
236
+ content: [{ type: 'text', text: `❌ Failed to save: ${data.error || 'Unknown error'}` }],
237
+ };
238
+ }
239
+ } catch (error) {
240
+ return {
241
+ content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
242
+ };
243
+ }
244
+ }
245
+
246
+ async listFiles(args) {
247
+ try {
248
+ const { passkey, isPublic = false } = args;
249
+
250
+ if (!isPublic && !passkey) {
251
+ return {
252
+ content: [{ type: 'text', text: '❌ Passkey is required (or set isPublic=true)' }],
253
+ };
254
  }
255
 
256
+ const url = new URL(API_ENDPOINT);
257
+ if (passkey) url.searchParams.set('passkey', passkey);
258
+ if (isPublic) url.searchParams.set('isPublic', 'true');
259
+
260
+ const response = await fetch(url, {
261
+ method: 'GET',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ });
264
+
265
+ const data = await response.json();
266
+
267
+ if (response.ok && data.success) {
268
+ if (data.files.length === 0) {
269
+ return {
270
+ content: [{ type: 'text', text: `📁 No files found in ${data.location}` }],
271
+ };
272
+ }
273
+
274
+ const fileList = data.files
275
+ .map(f => `📄 ${f.name} (${(f.size / 1024).toFixed(2)} KB)${f.isQuiz ? ' [QUIZ]' : ''}`)
276
+ .join('\n');
277
+
278
  return {
279
  content: [
280
  {
281
  type: 'text',
282
+ text: `📁 Files in ${data.location}:\n\n${fileList}\n\nTotal: ${data.count} file(s)`,
283
  },
284
  ],
285
  };
286
+ } else {
287
+ return {
288
+ content: [{ type: 'text', text: `❌ Failed: ${data.error || 'Unknown error'}` }],
289
+ };
290
  }
291
+ } catch (error) {
292
+ return {
293
+ content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
294
+ };
295
+ }
296
+ }
297
 
298
+ async deleteFile(args) {
299
+ try {
300
+ const { fileName, passkey, isPublic = false } = args;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ if (!fileName) {
303
+ return {
304
+ content: [{ type: 'text', text: '❌ fileName is required' }],
305
+ };
306
+ }
307
 
308
+ if (!isPublic && !passkey) {
309
+ return {
310
+ content: [{ type: 'text', text: '❌ Passkey is required (or set isPublic=true)' }],
311
+ };
312
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
+ const response = await fetch(API_ENDPOINT, {
315
+ method: 'POST',
316
+ headers: { 'Content-Type': 'application/json' },
317
+ body: JSON.stringify({
318
+ passkey,
319
+ action: 'delete_file',
320
+ fileName,
321
+ isPublic,
322
+ content: '', // Required by API but not used
323
+ }),
324
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
+ const data = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
+ if (response.ok && data.success) {
329
+ return {
330
+ content: [{ type: 'text', text: `✅ File '${fileName}' deleted successfully` }],
331
+ };
332
+ } else {
333
+ return {
334
+ content: [{ type: 'text', text: `❌ Failed: ${data.error || 'File not found'}` }],
335
+ };
 
336
  }
337
  } catch (error) {
 
338
  return {
339
+ content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
 
 
 
 
 
340
  };
341
  }
342
  }
343
 
344
  async deployQuiz(args) {
345
  try {
346
+ const { quizData, passkey, isPublic = false } = args;
347
+
348
+ if (!quizData || !quizData.questions || quizData.questions.length === 0) {
349
  return {
350
+ content: [{ type: 'text', text: '❌ Quiz must have at least one question' }],
351
+ };
352
+ }
353
+
354
+ if (!isPublic && !passkey) {
355
+ return {
356
+ content: [{ type: 'text', text: '❌ Passkey is required (or set isPublic=true)' }],
357
  };
358
  }
359
 
 
360
  const fullQuizData = {
361
+ ...quizData,
362
  createdAt: new Date().toISOString(),
363
+ passkey: passkey || 'public',
364
  version: '1.0',
365
  };
366
 
 
367
  const response = await fetch(API_ENDPOINT, {
368
  method: 'POST',
369
  headers: { 'Content-Type': 'application/json' },
370
  body: JSON.stringify({
371
+ passkey,
372
  action: 'deploy_quiz',
373
  fileName: 'quiz.json',
374
  content: JSON.stringify(fullQuizData, null, 2),
375
+ isPublic,
376
  }),
377
  });
378
 
379
  const data = await response.json();
380
 
381
  if (response.ok && data.success) {
382
+ const totalPoints = quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0);
383
+ const location = isPublic ? '📢 Public' : `🔐 Secure (Key: ${passkey})`;
384
 
385
  return {
386
  content: [
387
  {
388
  type: 'text',
389
+ text: `✅ Quiz deployed successfully!\n\n📝 Quiz: ${quizData.title}\n📊 Questions: ${quizData.questions.length}\n⏱️ Time Limit: ${quizData.timeLimit || 'No limit'} min\n🎯 Total Points: ${totalPoints}\n📍 Location: ${location}\n\n🚀 Quiz is now available in Reuben OS Quiz App!`,
390
  },
391
  ],
392
  };
393
  } else {
394
  return {
395
+ content: [{ type: 'text', text: `❌ Failed: ${data.error || 'Unknown error'}` }],
 
 
 
 
 
396
  };
397
  }
398
  } catch (error) {
 
399
  return {
400
+ content: [{ type: 'text', text: `❌ Error: ${error.message}` }],
 
 
 
 
 
401
  };
402
  }
403
  }
 
405
  async run() {
406
  const transport = new StdioServerTransport();
407
  await this.server.connect(transport);
408
+ console.error('ReubenOS MCP Server running with passkey authentication...');
409
  }
410
  }
411
 
pages/api/mcp-handler.js CHANGED
@@ -1,29 +1,25 @@
1
- // pages/api/mcp-handler.js
2
- import fs from 'fs/promises';
 
3
  import path from 'path';
4
 
 
 
 
 
5
  // Ensure directories exist
6
  async function ensureDirectories() {
7
- // Ensure /tmp directory exists
8
- const tmpPath = '/tmp';
9
- try {
10
- await fs.access(tmpPath);
11
- } catch {
12
- await fs.mkdir(tmpPath, { recursive: true });
13
  }
14
-
15
- // Ensure public uploads directory exists
16
- const publicPath = path.join(process.cwd(), 'public', 'uploads');
17
- try {
18
- await fs.access(publicPath);
19
- } catch {
20
- await fs.mkdir(publicPath, { recursive: true });
21
  }
22
  }
23
 
24
- // Validate session ID format (alphanumeric and underscores only)
25
- function isValidSessionId(sessionId) {
26
- return sessionId && /^[a-zA-Z0-9_-]+$/.test(sessionId);
27
  }
28
 
29
  // Sanitize filename to prevent path traversal
@@ -31,8 +27,13 @@ function sanitizeFileName(fileName) {
31
  return fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
32
  }
33
 
 
 
 
 
 
34
  export default async function handler(req, res) {
35
- // Enable CORS for all origins (adjust as needed for production)
36
  res.setHeader('Access-Control-Allow-Origin', '*');
37
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
38
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
@@ -45,14 +46,13 @@ export default async function handler(req, res) {
45
  await ensureDirectories();
46
 
47
  if (req.method === 'POST') {
48
- // Handle file save/quiz deployment
49
- const { sessionId, action, fileName, content } = req.body;
50
 
51
- // Validate session ID
52
- if (!isValidSessionId(sessionId)) {
53
  return res.status(400).json({
54
  success: false,
55
- error: 'Invalid or missing sessionId'
56
  });
57
  }
58
 
@@ -67,51 +67,56 @@ export default async function handler(req, res) {
67
  const sanitizedFileName = sanitizeFileName(fileName);
68
 
69
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  // Handle different actions
71
  switch (action) {
72
  case 'save_file':
73
- case 'deploy_quiz': {
74
- // Save to /tmp with session prefix
75
- const prefixedFileName = `${sessionId}_${sanitizedFileName}`;
76
- const filePath = path.join('/tmp', prefixedFileName);
77
-
78
  await fs.writeFile(filePath, content, 'utf8');
79
 
80
  return res.status(200).json({
81
  success: true,
82
- message: `File saved successfully`,
83
- fileName: sanitizedFileName,
84
- prefixedFileName: prefixedFileName,
85
- action: action
86
- });
87
- }
88
-
89
- case 'save_public': {
90
- // Save to public/uploads without session prefix
91
- const publicPath = path.join(process.cwd(), 'public', 'uploads', sanitizedFileName);
92
-
93
- await fs.writeFile(publicPath, content, 'utf8');
94
-
95
- return res.status(200).json({
96
- success: true,
97
- message: `File saved to public folder`,
98
  fileName: sanitizedFileName,
99
- isPublic: true,
100
- action: action
 
 
 
101
  });
102
  }
103
 
104
- case 'delete_file': {
105
- // Delete specific file from /tmp
106
- const prefixedFileName = `${sessionId}_${sanitizedFileName}`;
107
- const filePath = path.join('/tmp', prefixedFileName);
108
 
109
  try {
110
  await fs.unlink(filePath);
111
  return res.status(200).json({
112
  success: true,
113
  message: `File deleted successfully`,
114
- fileName: sanitizedFileName
 
115
  });
116
  } catch (err) {
117
  return res.status(404).json({
@@ -122,19 +127,29 @@ export default async function handler(req, res) {
122
  }
123
 
124
  case 'clear_session':
125
- // Clear all files for this session
126
- const files = await fs.readdir('/tmp');
127
- const sessionFiles = files.filter(f => f.startsWith(`${sessionId}_`));
 
 
 
 
128
 
129
- for (const file of sessionFiles) {
130
- await fs.unlink(path.join('/tmp', file));
 
 
 
 
131
  }
132
 
133
  return res.status(200).json({
134
  success: true,
135
- message: `Cleared ${sessionFiles.length} files for session`,
136
- deletedCount: sessionFiles.length
 
137
  });
 
138
 
139
  default:
140
  return res.status(400).json({
@@ -151,37 +166,51 @@ export default async function handler(req, res) {
151
  }
152
 
153
  } else if (req.method === 'GET') {
154
- // Handle file retrieval
155
- const { sessionId } = req.query;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- // Validate session ID
158
- if (!isValidSessionId(sessionId)) {
159
- return res.status(400).json({
160
- success: false,
161
- error: 'Invalid or missing sessionId'
162
- });
 
 
 
 
 
 
 
 
 
163
  }
164
 
165
  try {
166
- // Read all files from /tmp
167
- const allFiles = await fs.readdir('/tmp');
168
 
169
- // Filter files for this session
170
- const sessionPrefix = `${sessionId}_`;
171
- const sessionFiles = allFiles.filter(f => f.startsWith(sessionPrefix));
172
-
173
- // Build file list with content
174
  const fileList = await Promise.all(
175
- sessionFiles.map(async (file) => {
176
- const filePath = path.join('/tmp', file);
177
  const stats = await fs.stat(filePath);
178
 
179
- // Strip session prefix from filename
180
- const cleanFileName = file.substring(sessionPrefix.length);
181
-
182
- // Read file content (with size limit to prevent memory issues)
183
  let content = null;
184
- if (stats.size < 1024 * 1024) { // 1MB limit for content retrieval
185
  try {
186
  content = await fs.readFile(filePath, 'utf8');
187
  } catch (err) {
@@ -190,12 +219,12 @@ export default async function handler(req, res) {
190
  }
191
 
192
  return {
193
- name: cleanFileName,
194
- prefixedName: file,
195
  size: stats.size,
196
  modified: stats.mtime,
197
- isQuiz: cleanFileName === 'quiz.json',
198
- content: content
 
199
  };
200
  })
201
  );
@@ -205,9 +234,9 @@ export default async function handler(req, res) {
205
 
206
  return res.status(200).json({
207
  success: true,
208
- sessionId: sessionId,
209
  files: fileList,
210
- count: fileList.length
 
211
  });
212
 
213
  } catch (error) {
 
1
+ // pages/api/mcp-handler.js - Updated for Passkey System
2
+ import fs from 'fs/promises'
3
+ import fssync from 'fs';
4
  import path from 'path';
5
 
6
+ // Use /data for Hugging Face Spaces persistent storage
7
+ const DATA_DIR = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'public', 'data');
8
+ const PUBLIC_DIR = path.join(DATA_DIR, 'public');
9
+
10
  // Ensure directories exist
11
  async function ensureDirectories() {
12
+ if (!fssync.existsSync(DATA_DIR)) {
13
+ await fs.mkdir(DATA_DIR, { recursive: true });
 
 
 
 
14
  }
15
+ if (!fssync.existsSync(PUBLIC_DIR)) {
16
+ await fs.mkdir(PUBLIC_DIR, { recursive: true });
 
 
 
 
 
17
  }
18
  }
19
 
20
+ // Validate passkey format (alphanumeric and hyphens/underscores only)
21
+ function isValidPasskey(passkey) {
22
+ return passkey && /^[a-zA-Z0-9_-]+$/.test(passkey) && passkey.length >= 4;
23
  }
24
 
25
  // Sanitize filename to prevent path traversal
 
27
  return fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
28
  }
29
 
30
+ // Sanitize passkey (used for directory names)
31
+ function sanitizePasskey(passkey) {
32
+ return passkey.replace(/[^a-zA-Z0-9_-]/g, '');
33
+ }
34
+
35
  export default async function handler(req, res) {
36
+ // Enable CORS
37
  res.setHeader('Access-Control-Allow-Origin', '*');
38
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
39
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
 
46
  await ensureDirectories();
47
 
48
  if (req.method === 'POST') {
49
+ const { passkey, action, fileName, content, isPublic = false } = req.body;
 
50
 
51
+ // Public files don't need passkey
52
+ if (!isPublic && !isValidPasskey(passkey)) {
53
  return res.status(400).json({
54
  success: false,
55
+ error: 'Invalid or missing passkey (minimum 4 characters required)'
56
  });
57
  }
58
 
 
67
  const sanitizedFileName = sanitizeFileName(fileName);
68
 
69
  try {
70
+ let targetDir;
71
+ let responseData = {};
72
+
73
+ if (isPublic) {
74
+ targetDir = PUBLIC_DIR;
75
+ responseData.isPublic = true;
76
+ responseData.location = 'Public Files';
77
+ } else {
78
+ const sanitizedKey = sanitizePasskey(passkey);
79
+ targetDir = path.join(DATA_DIR, sanitizedKey);
80
+
81
+ if (!fssync.existsSync(targetDir)) {
82
+ await fs.mkdir(targetDir, { recursive: true });
83
+ }
84
+
85
+ responseData.passkey = sanitizedKey;
86
+ responseData.location = 'Secure Data';
87
+ }
88
+
89
  // Handle different actions
90
  switch (action) {
91
  case 'save_file':
92
+ case 'deploy_quiz':
93
+ case 'save': {
94
+ const filePath = path.join(targetDir, sanitizedFileName);
 
 
95
  await fs.writeFile(filePath, content, 'utf8');
96
 
97
  return res.status(200).json({
98
  success: true,
99
+ message: `File saved successfully to ${responseData.location}`,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  fileName: sanitizedFileName,
101
+ action: action,
102
+ ...responseData,
103
+ url: isPublic
104
+ ? `${process.env.SPACE_ID ? 'https://mcp-1st-birthday-reuben-os.hf.space' : 'http://localhost:3000'}/data/public/${sanitizedFileName}`
105
+ : null
106
  });
107
  }
108
 
109
+ case 'delete_file':
110
+ case 'delete': {
111
+ const filePath = path.join(targetDir, sanitizedFileName);
 
112
 
113
  try {
114
  await fs.unlink(filePath);
115
  return res.status(200).json({
116
  success: true,
117
  message: `File deleted successfully`,
118
+ fileName: sanitizedFileName,
119
+ ...responseData
120
  });
121
  } catch (err) {
122
  return res.status(404).json({
 
127
  }
128
 
129
  case 'clear_session':
130
+ case 'clear': {
131
+ if (isPublic) {
132
+ return res.status(400).json({
133
+ success: false,
134
+ error: 'Cannot clear public folder via this endpoint'
135
+ });
136
+ }
137
 
138
+ const files = await fs.readdir(targetDir);
139
+ let deletedCount = 0;
140
+
141
+ for (const file of files) {
142
+ await fs.unlink(path.join(targetDir, file));
143
+ deletedCount++;
144
  }
145
 
146
  return res.status(200).json({
147
  success: true,
148
+ message: `Cleared ${deletedCount} files`,
149
+ deletedCount,
150
+ ...responseData
151
  });
152
+ }
153
 
154
  default:
155
  return res.status(400).json({
 
166
  }
167
 
168
  } else if (req.method === 'GET') {
169
+ const { passkey, isPublic } = req.query;
170
+
171
+ let targetDir;
172
+ let responseData = {};
173
+
174
+ if (isPublic === 'true') {
175
+ targetDir = PUBLIC_DIR;
176
+ responseData.isPublic = true;
177
+ responseData.location = 'Public Files';
178
+ } else {
179
+ if (!isValidPasskey(passkey)) {
180
+ return res.status(400).json({
181
+ success: false,
182
+ error: 'Invalid or missing passkey'
183
+ });
184
+ }
185
 
186
+ const sanitizedKey = sanitizePasskey(passkey);
187
+ targetDir = path.join(DATA_DIR, sanitizedKey);
188
+
189
+ if (!fssync.existsSync(targetDir)) {
190
+ return res.status(200).json({
191
+ success: true,
192
+ passkey: sanitizedKey,
193
+ files: [],
194
+ count: 0,
195
+ message: 'No files found for this passkey'
196
+ });
197
+ }
198
+
199
+ responseData.passkey = sanitizedKey;
200
+ responseData.location = 'Secure Data';
201
  }
202
 
203
  try {
204
+ const allFiles = await fs.readdir(targetDir);
 
205
 
 
 
 
 
 
206
  const fileList = await Promise.all(
207
+ allFiles.map(async (file) => {
208
+ const filePath = path.join(targetDir, file);
209
  const stats = await fs.stat(filePath);
210
 
211
+ // Read file content (with size limit)
 
 
 
212
  let content = null;
213
+ if (stats.size < 1024 * 1024) { // 1MB limit
214
  try {
215
  content = await fs.readFile(filePath, 'utf8');
216
  } catch (err) {
 
219
  }
220
 
221
  return {
222
+ name: file,
 
223
  size: stats.size,
224
  modified: stats.mtime,
225
+ isQuiz: file === 'quiz.json',
226
+ content: content,
227
+ extension: path.extname(file).substring(1)
228
  };
229
  })
230
  );
 
234
 
235
  return res.status(200).json({
236
  success: true,
 
237
  files: fileList,
238
+ count: fileList.length,
239
+ ...responseData
240
  });
241
 
242
  } catch (error) {