Spaces:
Running
Running
feat: Update MCP integration to use passkey system instead of sessionId
Browse files- CLAUDE_INTEGRATION_GUIDE.md +362 -0
- mcp-server.js +219 -246
- 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 -
|
| 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: '
|
| 21 |
-
description: '
|
| 22 |
icon: '🖥️',
|
| 23 |
},
|
| 24 |
{
|
|
@@ -42,94 +42,110 @@ class ReubenOSMCPServer {
|
|
| 42 |
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
| 43 |
tools: [
|
| 44 |
{
|
| 45 |
-
name: '
|
| 46 |
-
description: '
|
| 47 |
inputSchema: {
|
| 48 |
type: 'object',
|
| 49 |
properties: {
|
| 50 |
-
|
| 51 |
type: 'string',
|
| 52 |
-
description: '
|
| 53 |
},
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
type: 'string',
|
| 56 |
-
|
| 57 |
-
description: 'Action to perform (use save_public to save to public folder)',
|
| 58 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
fileName: {
|
| 60 |
type: 'string',
|
| 61 |
-
description: '
|
| 62 |
},
|
| 63 |
-
|
| 64 |
type: 'string',
|
| 65 |
-
description: '
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
},
|
| 67 |
},
|
| 68 |
-
required: ['
|
| 69 |
},
|
| 70 |
},
|
| 71 |
{
|
| 72 |
name: 'deploy_quiz',
|
| 73 |
-
description: 'Deploy an interactive quiz to Reuben OS
|
| 74 |
inputSchema: {
|
| 75 |
type: 'object',
|
| 76 |
properties: {
|
| 77 |
-
|
| 78 |
type: 'string',
|
| 79 |
-
description: '
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
},
|
| 81 |
quizData: {
|
| 82 |
type: 'object',
|
| 83 |
-
description: 'Quiz
|
| 84 |
properties: {
|
| 85 |
-
title: {
|
| 86 |
-
|
| 87 |
-
|
| 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
|
| 100 |
items: {
|
| 101 |
type: 'object',
|
| 102 |
properties: {
|
| 103 |
-
id: {
|
| 104 |
-
|
| 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 |
-
|
| 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: ['
|
| 142 |
},
|
| 143 |
},
|
| 144 |
],
|
|
@@ -149,8 +165,12 @@ class ReubenOSMCPServer {
|
|
| 149 |
|
| 150 |
try {
|
| 151 |
switch (name) {
|
| 152 |
-
case '
|
| 153 |
-
return await this.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
case 'deploy_quiz':
|
| 155 |
return await this.deployQuiz(args);
|
| 156 |
default:
|
|
@@ -169,262 +189,215 @@ class ReubenOSMCPServer {
|
|
| 169 |
});
|
| 170 |
}
|
| 171 |
|
| 172 |
-
async
|
| 173 |
try {
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
return {
|
| 177 |
content: [
|
| 178 |
{
|
| 179 |
type: 'text',
|
| 180 |
-
text:
|
| 181 |
},
|
| 182 |
],
|
| 183 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
return {
|
| 188 |
content: [
|
| 189 |
{
|
| 190 |
type: 'text',
|
| 191 |
-
text:
|
| 192 |
},
|
| 193 |
],
|
| 194 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 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 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 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 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 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 |
-
|
| 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 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 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 |
-
|
| 365 |
-
|
|
|
|
| 366 |
return {
|
| 367 |
-
content: [
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
| 373 |
};
|
| 374 |
}
|
| 375 |
|
| 376 |
-
// Add metadata to quiz
|
| 377 |
const fullQuizData = {
|
| 378 |
-
...
|
| 379 |
createdAt: new Date().toISOString(),
|
| 380 |
-
|
| 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 |
-
|
| 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 =
|
|
|
|
| 400 |
|
| 401 |
return {
|
| 402 |
content: [
|
| 403 |
{
|
| 404 |
type: 'text',
|
| 405 |
-
text: `✅ Quiz deployed successfully!\n\n📝 Quiz: ${
|
| 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 |
-
|
| 8 |
-
|
| 9 |
-
try {
|
| 10 |
-
await fs.access(tmpPath);
|
| 11 |
-
} catch {
|
| 12 |
-
await fs.mkdir(tmpPath, { recursive: true });
|
| 13 |
}
|
| 14 |
-
|
| 15 |
-
|
| 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
|
| 25 |
-
function
|
| 26 |
-
return
|
| 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
|
| 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 |
-
|
| 49 |
-
const { sessionId, action, fileName, content } = req.body;
|
| 50 |
|
| 51 |
-
//
|
| 52 |
-
if (!
|
| 53 |
return res.status(400).json({
|
| 54 |
success: false,
|
| 55 |
-
error: 'Invalid or missing
|
| 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 |
-
|
| 75 |
-
const
|
| 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 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
});
|
| 102 |
}
|
| 103 |
|
| 104 |
-
case 'delete_file':
|
| 105 |
-
|
| 106 |
-
const
|
| 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 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
return res.status(200).json({
|
| 134 |
success: true,
|
| 135 |
-
message: `Cleared ${
|
| 136 |
-
deletedCount
|
|
|
|
| 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 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
try {
|
| 166 |
-
|
| 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 |
-
|
| 176 |
-
const filePath = path.join(
|
| 177 |
const stats = await fs.stat(filePath);
|
| 178 |
|
| 179 |
-
//
|
| 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
|
| 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:
|
| 194 |
-
prefixedName: file,
|
| 195 |
size: stats.size,
|
| 196 |
modified: stats.mtime,
|
| 197 |
-
isQuiz:
|
| 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) {
|