@adiholick/nvstudio-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/dist/index.js +141 -0
- package/dist/roblox-bridge.js +51 -0
- package/dist/task-queue.js +46 -0
- package/package.json +36 -0
- package/public/index.html +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# nvstudio-mcp 🚀
|
|
2
|
+
|
|
3
|
+
**nvstudio-mcp** adalah jembatan pintar (*Model Context Protocol Bridge*) yang menghubungkan Asisten AI Anda (seperti Antigravity IDE, Cursor, Claude Desktop, atau Windsurf) secara langsung ke dalam **Roblox Studio**. Dengan plugin ini, AI Anda dapat membaca, mengedit script, men-generate terrain, dan berinteraksi dengan API Roblox Studio layaknya seorang asisten *Co-Pilot* sejati.
|
|
4
|
+
|
|
5
|
+
Sistem ini didesain menggunakan **Hybrid Architecture** yang memisahkan logika Engine (*Studio Plugin*) dan aturan anti-halusinasi (*Agent Plugin*).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📥 Instalasi
|
|
10
|
+
|
|
11
|
+
Langkah instalasinya **sangat gampang!**
|
|
12
|
+
|
|
13
|
+
### 1. Jalankan Installer (Otomatis)
|
|
14
|
+
|
|
15
|
+
Pilih perintah sesuai terminal yang Anda gunakan:
|
|
16
|
+
|
|
17
|
+
**Windows (PowerShell):**
|
|
18
|
+
```powershell
|
|
19
|
+
irm https://raw.githubusercontent.com/Adiholick/nvstudio-mcp/main/install.ps1 | iex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**macOS / Linux / Git Bash:**
|
|
23
|
+
```bash
|
|
24
|
+
curl -fsSL https://raw.githubusercontent.com/Adiholick/nvstudio-mcp/main/install.sh | bash
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
**Installer ini akan secara otomatis melakukan hal berikut:**
|
|
29
|
+
- Memasang dependensi Node.js dan melakukan kompilasi TypeScript (*Bridge Server*).
|
|
30
|
+
- Menyalin file plugin `nvstudio_mcp.rbxmx` ke dalam sistem Plugin Roblox Studio di OS Anda (Windows/macOS/WSL) secara otomatis.
|
|
31
|
+
- Mengintegrasikan *AI Agent Skills* langsung ke **Antigravity IDE** (`~/.gemini/config/plugins/nvstudio-mcp`).
|
|
32
|
+
- Melakukan auto-konfigurasi JSON jika Anda menggunakan **Cursor Editor**.
|
|
33
|
+
|
|
34
|
+
### 2. Mengaktifkan Izin di Roblox Studio
|
|
35
|
+
1. Buka proyek game Anda di Roblox Studio.
|
|
36
|
+
2. Di pojok kiri atas, buka **Home** -> **Game Settings** -> **Security**.
|
|
37
|
+
3. Centang (aktifkan) opsi **`Allow HTTP Requests`** agar plugin dapat berkomunikasi dengan AI.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🔌 Cara Menggunakan (Untuk AI Lain)
|
|
42
|
+
|
|
43
|
+
Jika Anda menggunakan AI Client lain (misalnya: **Claude Desktop** atau klien yang tidak dikonfigurasi otomatis), Anda dapat menambahkan `nvstudio-mcp` secara manual ke konfigurasi `mcp.json` Anda.
|
|
44
|
+
|
|
45
|
+
Salin konfigurasi ini:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"nvstudio-mcp": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": [
|
|
52
|
+
"C:/Path/Ke/Folder/nvstudio-mcp/dist/index.js"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
*(Ingat untuk mengganti path `args` di atas dengan direktori instalasi aktual Anda, biasanya berada di `~/.nvstudio-mcp/dist/index.js`).*
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🗑️ Cara Uninstall (Mencopot Pemasangan)
|
|
63
|
+
|
|
64
|
+
Jika Anda ingin berhenti menggunakan `nvstudio-mcp`, Anda dapat membersihkannya dari sistem Anda secara menyeluruh dengan 4 langkah berikut:
|
|
65
|
+
|
|
66
|
+
### 1. Hapus Plugin dari Roblox Studio
|
|
67
|
+
Hapus file plugin dari folder lokal Roblox Anda.
|
|
68
|
+
- **Windows:** Buka folder `%LOCALAPPDATA%\Roblox\Plugins` (atau `AppData\Local\Roblox\Plugins`) dan hapus file `nvstudio_mcp.rbxmx` (atau `nvstudio_mcp.rbxm`).
|
|
69
|
+
- **macOS:** Buka folder `~/Documents/ROBLOX/Plugins` dan hapus file `nvstudio_mcp.rbxmx` (atau `nvstudio_mcp.rbxm`).
|
|
70
|
+
|
|
71
|
+
### 2. Hapus Agen dari Antigravity (Jika Menggunakan AGY)
|
|
72
|
+
Hapus *bundle* keterampilan *anti-halusinasi* dari memori AI Anda:
|
|
73
|
+
```bash
|
|
74
|
+
rm -rf ~/.gemini/config/plugins/nvstudio-mcp
|
|
75
|
+
```
|
|
76
|
+
*(Untuk pengguna Windows PowerShell, Anda bisa menghapus folder `C:\Users\NamaAnda\.gemini\config\plugins\nvstudio-mcp` secara manual).*
|
|
77
|
+
|
|
78
|
+
### 3. Hapus File Instalasi Node.js
|
|
79
|
+
Installer `install.sh` sebelumnya mengkloning repositori dan menginstal Node.js di dalam direktori *Home* Anda (secara bawaan di `~/.nvstudio-mcp`). Hapus direktori tersebut:
|
|
80
|
+
```bash
|
|
81
|
+
rm -rf ~/.nvstudio-mcp
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Hapus Konfigurasi dari Editor AI Anda
|
|
85
|
+
Jika Anda memasukkan konfigurasi MCP secara otomatis ke Cursor atau secara manual ke Claude Desktop, buka file pengaturan `mcp.json` tersebut dan **hapus blok `"nvstudio-mcp"`** dari dalam objek `mcpServers`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
**Selamat Menciptakan Game Impian Anda dengan AI! 🎮🤖**
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
7
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
9
|
+
const roblox_bridge_1 = require("./roblox-bridge");
|
|
10
|
+
const task_queue_1 = require("./task-queue");
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const HISTORY_DIR = path_1.default.join(process.cwd(), '.history');
|
|
14
|
+
if (!fs_1.default.existsSync(HISTORY_DIR)) {
|
|
15
|
+
fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
function backupScript(target, code) {
|
|
18
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
19
|
+
const safeTargetName = target.replace(/[^a-zA-Z0-9]/g, '_');
|
|
20
|
+
const fileName = `${safeTargetName}_${timestamp}.luau`;
|
|
21
|
+
fs_1.default.writeFileSync(path_1.default.join(HISTORY_DIR, fileName), code);
|
|
22
|
+
}
|
|
23
|
+
// 1. Impor dan jalankan server Express lokal di port 3055
|
|
24
|
+
(0, roblox_bridge_1.startBridgeServer)(3055);
|
|
25
|
+
// 2. Inisialisasi server MCP
|
|
26
|
+
const server = new index_js_1.Server({
|
|
27
|
+
name: "nvstudio-mcp",
|
|
28
|
+
version: "1.0.0",
|
|
29
|
+
}, {
|
|
30
|
+
capabilities: {
|
|
31
|
+
tools: {},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
// 3 & 4. Daftarkan tool call_mcp_tool dan definisikan schema parameternya
|
|
35
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
36
|
+
return {
|
|
37
|
+
tools: [
|
|
38
|
+
{
|
|
39
|
+
name: "call_mcp_tool",
|
|
40
|
+
description: "ALAT MUTLAK UNTUK ROBLOX. DILARANG KERAS menggunakan PowerShell atau skrip bypass (fetch_mcp_source.js). WAJIB memanggil 'get_children' terlebih dahulu sebelum 'get_script_source' untuk memverifikasi tipe instance agar tidak terjadi error teks kosong.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
command: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Perintah yang akan dieksekusi (contoh: 'get_children', 'get_script_source', 'update_script_source', 'rollback_script', 'get_logs').",
|
|
47
|
+
},
|
|
48
|
+
target: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Path absolut dari objek target di Roblox Studio (contoh: 'Workspace.Map.Script' atau 'ServerScriptService.MainModule').",
|
|
51
|
+
},
|
|
52
|
+
data: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Data tambahan opsional, misalnya isi source code baru saat menjalankan perintah 'update_script_source'.",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ["command", "target"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
// 5. Handler untuk menangani pemanggilan call_mcp_tool
|
|
64
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
65
|
+
if (request.params.name !== "call_mcp_tool") {
|
|
66
|
+
throw new Error(`Tool tidak dikenali: ${request.params.name}`);
|
|
67
|
+
}
|
|
68
|
+
const args = request.params.arguments;
|
|
69
|
+
const { command, target, data } = args;
|
|
70
|
+
if (!command || !target) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify({
|
|
76
|
+
status: "error",
|
|
77
|
+
error: "Parameter 'command' dan 'target' wajib diisi.",
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
isError: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// 6. 🔴 GUARDRAIL MUTLAK: Cegah tebakan path buta
|
|
85
|
+
if (command === "get_script_source") {
|
|
86
|
+
const genericTargets = ["Workspace", "ServerScriptService", "ReplicatedStorage", "StarterGui", "StarterPack", "game"];
|
|
87
|
+
if (genericTargets.includes(target) || !target.includes(".")) {
|
|
88
|
+
return {
|
|
89
|
+
content: [{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: "🔴 SERVER GUARDRAIL BLOCKED: DILARANG mengekstrak kode secara buta dari direktori utama atau target yang tidak spesifik. Anda WAJIB memanggil perintah 'get_children' pada '" + target + "' terlebih dahulu untuk memverifikasi nama dan ClassName (Pastikan target adalah LuaSourceContainer)."
|
|
92
|
+
}],
|
|
93
|
+
isError: true
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 7. Teruskan ke antrean dan kembalikan hasilnya sebagai string JSON
|
|
98
|
+
try {
|
|
99
|
+
if (command === "update_script_source" && data) {
|
|
100
|
+
backupScript(target, String(data));
|
|
101
|
+
}
|
|
102
|
+
const result = await (0, task_queue_1.addTaskToQueue)(command, target, data);
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: JSON.stringify({
|
|
118
|
+
status: "error",
|
|
119
|
+
error: err?.message || String(err),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// 8. Jalankan koneksi MCP menggunakan StdioServerTransport
|
|
128
|
+
async function main() {
|
|
129
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
130
|
+
await server.connect(transport);
|
|
131
|
+
console.error("[nvstudio-mcp] MCP Server aktif melalui StdioServerTransport.");
|
|
132
|
+
// Tangani penutupan koneksi (misal IDE ditutup) agar server ikut mati
|
|
133
|
+
process.stdin.on('close', () => {
|
|
134
|
+
console.error("[nvstudio-mcp] Koneksi stdin ditutup oleh IDE. Mematikan server...");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
main().catch((err) => {
|
|
139
|
+
console.error("[nvstudio-mcp] Gagal menjalankan MCP server:", err);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startBridgeServer = startBridgeServer;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const http_1 = require("http");
|
|
9
|
+
const socket_io_1 = require("socket.io");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const task_queue_1 = require("./task-queue");
|
|
13
|
+
function startBridgeServer(port = 3055) {
|
|
14
|
+
const app = (0, express_1.default)();
|
|
15
|
+
const httpServer = (0, http_1.createServer)(app);
|
|
16
|
+
const io = new socket_io_1.Server(httpServer, { cors: { origin: "*" } });
|
|
17
|
+
// FIX: Menaikkan limit ke 50mb agar file .Source (Lua) yang besar tidak melempar error 413 Payload Too Large
|
|
18
|
+
app.use(express_1.default.json({ limit: '50mb' }));
|
|
19
|
+
app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
|
|
20
|
+
let dashboardOpened = false;
|
|
21
|
+
// Endpoint Studio Polling
|
|
22
|
+
app.get('/api/tasks', (req, res) => {
|
|
23
|
+
if (!dashboardOpened) {
|
|
24
|
+
dashboardOpened = true;
|
|
25
|
+
const startCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
26
|
+
(0, child_process_1.exec)(`${startCmd} http://localhost:${port}`);
|
|
27
|
+
console.error(`[Bridge] Pertama kali terhubung dengan Studio! Membuka Dashboard...`);
|
|
28
|
+
}
|
|
29
|
+
if (task_queue_1.taskQueue.length > 0) {
|
|
30
|
+
const task = task_queue_1.taskQueue.shift();
|
|
31
|
+
io.emit('log', `[AI Task] Mengirim perintah '${task?.command}' ke Studio.`);
|
|
32
|
+
res.json(task);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
res.json({ id: null });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// Endpoint Studio Response
|
|
39
|
+
app.post('/api/response', (req, res) => {
|
|
40
|
+
const { id, status, result, error } = req.body;
|
|
41
|
+
io.emit('log', `[Studio Response] Status: ${status}`);
|
|
42
|
+
if (error)
|
|
43
|
+
io.emit('log', `[Error] ${error}`);
|
|
44
|
+
// Memanggil fungsi resolve secara sinkron (menghindari promise import circular palsu)
|
|
45
|
+
(0, task_queue_1.resolvePendingTask)(id, status, result, error);
|
|
46
|
+
res.json({ message: "Response diterima" });
|
|
47
|
+
});
|
|
48
|
+
httpServer.listen(port, () => {
|
|
49
|
+
console.error(`[Bridge] Dashboard visual & server aktif di http://localhost:${port}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pendingTasks = exports.taskQueue = void 0;
|
|
4
|
+
exports.addTaskToQueue = addTaskToQueue;
|
|
5
|
+
exports.resolvePendingTask = resolvePendingTask;
|
|
6
|
+
const uuid_1 = require("uuid");
|
|
7
|
+
// 1. Array untuk menampung antrean tugas (task queue)
|
|
8
|
+
exports.taskQueue = [];
|
|
9
|
+
// 2. Map untuk menyimpan Promise dari tugas yang sedang menunggu (pending tasks)
|
|
10
|
+
exports.pendingTasks = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Menambahkan task baru ke antrean dan mengembalikan Promise
|
|
13
|
+
* yang tertahan sampai ada response atau timeout 30 detik.
|
|
14
|
+
*/
|
|
15
|
+
function addTaskToQueue(command, target, data) {
|
|
16
|
+
const id = (0, uuid_1.v4)();
|
|
17
|
+
const task = { id, command, target, data };
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
if (exports.pendingTasks.has(id)) {
|
|
21
|
+
exports.pendingTasks.delete(id);
|
|
22
|
+
reject(new Error("Timeout: Roblox Studio tidak merespons"));
|
|
23
|
+
}
|
|
24
|
+
}, 30000);
|
|
25
|
+
exports.pendingTasks.set(id, { resolve, reject, timer });
|
|
26
|
+
exports.taskQueue.push(task);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Menyelesaikan pending task berdasarkan id dan status respons dari Roblox Studio.
|
|
31
|
+
*/
|
|
32
|
+
function resolvePendingTask(id, status, result, error) {
|
|
33
|
+
const pending = exports.pendingTasks.get(id);
|
|
34
|
+
if (!pending) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
clearTimeout(pending.timer);
|
|
38
|
+
exports.pendingTasks.delete(id);
|
|
39
|
+
if (status === "success") {
|
|
40
|
+
pending.resolve(result);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
pending.reject(new Error(error || "Terjadi kesalahan pada Roblox Studio"));
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adiholick/nvstudio-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local Bridge Server MCP for Roblox Studio",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"public"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "ts-node src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"roblox",
|
|
18
|
+
"studio"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
24
|
+
"express": "^5.2.1",
|
|
25
|
+
"socket.io": "^4.8.3",
|
|
26
|
+
"uuid": "^14.0.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/express": "^5.0.6",
|
|
30
|
+
"@types/node": "^26.4.1",
|
|
31
|
+
"@types/socket.io": "^3.0.1",
|
|
32
|
+
"@types/uuid": "^10.0.0",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"typescript": "^7.0.2"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>nvstudio-mcp | Live Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: 'Consolas', monospace; background-color: #0f172a; color: #38bdf8; margin: 0; padding: 20px; }
|
|
9
|
+
h1 { color: #f8fafc; font-size: 1.5rem; border-bottom: 1px solid #334155; padding-bottom: 10px; }
|
|
10
|
+
#log-container { background-color: #1e293b; padding: 15px; border-radius: 8px; height: 75vh; overflow-y: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
|
|
11
|
+
.log-entry { margin-bottom: 8px; border-bottom: 1px solid #334155; padding-bottom: 4px; }
|
|
12
|
+
.log-time { color: #94a3b8; font-size: 0.85em; margin-right: 10px; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<h1>nvstudio-mcp : Activity Dashboard (Port 3055)</h1>
|
|
17
|
+
<div id="log-container"></div>
|
|
18
|
+
|
|
19
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
20
|
+
<script>
|
|
21
|
+
const socket = io();
|
|
22
|
+
const container = document.getElementById('log-container');
|
|
23
|
+
|
|
24
|
+
socket.on('log', (msg) => {
|
|
25
|
+
const el = document.createElement('div');
|
|
26
|
+
el.className = 'log-entry';
|
|
27
|
+
const time = new Date().toLocaleTimeString();
|
|
28
|
+
el.innerHTML = `<span class="log-time">[${time}]</span>${msg}`;
|
|
29
|
+
container.appendChild(el);
|
|
30
|
+
container.scrollTop = container.scrollHeight; // Auto-scroll
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|