portals-mcp 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +23 -26
- package/dist/auth.js +10 -70
- package/dist/tools.js +17 -65
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fetch from "node-fetch";
|
|
2
|
-
const API_BASE = "
|
|
2
|
+
const API_BASE = "http://theportal.to";
|
|
3
3
|
if (!API_BASE) {
|
|
4
4
|
throw new Error("API_BASE_URL env var is required");
|
|
5
5
|
}
|
|
@@ -13,26 +13,19 @@ export function getAccessKey() {
|
|
|
13
13
|
}
|
|
14
14
|
return accessKey;
|
|
15
15
|
}
|
|
16
|
-
export async function
|
|
17
|
-
const res = await fetch(`${API_BASE}/api/v2/mcp/
|
|
16
|
+
export async function uploadRoomData(roomId, data) {
|
|
17
|
+
const res = await fetch(`${API_BASE}/api/v2/mcp/upload-room-data`, {
|
|
18
18
|
method: "POST",
|
|
19
|
-
headers: {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return json.data;
|
|
26
|
-
}
|
|
27
|
-
export async function setRoomData(roomId, data) {
|
|
28
|
-
const res = await fetch(`${API_BASE}/api/v2/mcp/set-room-data`, {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: { "Content-Type": "application/json" },
|
|
31
|
-
body: JSON.stringify({ roomId, accessKey: getAccessKey(), data }),
|
|
19
|
+
headers: {
|
|
20
|
+
"x-room-id": roomId,
|
|
21
|
+
"x-access-key": getAccessKey(),
|
|
22
|
+
"Content-Type": "application/octet-stream",
|
|
23
|
+
},
|
|
24
|
+
body: data,
|
|
32
25
|
});
|
|
33
26
|
const json = await res.json();
|
|
34
27
|
if (!res.ok)
|
|
35
|
-
throw new Error(json.message ?? json.error ?? "Failed to
|
|
28
|
+
throw new Error(json.message ?? json.error ?? "Failed to upload room data");
|
|
36
29
|
}
|
|
37
30
|
export async function validateAccessKey(accessKey) {
|
|
38
31
|
const res = await fetch(`${API_BASE}/api/v2/mcp/verify-access-key`, {
|
|
@@ -45,14 +38,18 @@ export async function validateAccessKey(accessKey) {
|
|
|
45
38
|
throw new Error(json.message ?? json.error ?? "Invalid access key");
|
|
46
39
|
return json.data.uid;
|
|
47
40
|
}
|
|
48
|
-
export async function
|
|
49
|
-
const res = await fetch(`${API_BASE}/api/v2/mcp/
|
|
50
|
-
method: "
|
|
51
|
-
headers: {
|
|
52
|
-
|
|
41
|
+
export async function downloadRoomData(roomId) {
|
|
42
|
+
const res = await fetch(`${API_BASE}/api/v2/mcp/download-room-data`, {
|
|
43
|
+
method: "GET",
|
|
44
|
+
headers: {
|
|
45
|
+
"x-room-id": roomId,
|
|
46
|
+
"x-access-key": getAccessKey(),
|
|
47
|
+
},
|
|
53
48
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new Error(json.message ?? json.error ?? "Failed to
|
|
57
|
-
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const json = await res.json();
|
|
51
|
+
throw new Error(json.message ?? json.error ?? "Failed to download room data");
|
|
52
|
+
}
|
|
53
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
54
|
+
return Buffer.from(arrayBuffer);
|
|
58
55
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,76 +1,16 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { exec } from "node:child_process";
|
|
3
3
|
import { validateAccessKey } from "./api.js";
|
|
4
|
-
const
|
|
5
|
-
<html lang="en">
|
|
6
|
-
<head>
|
|
7
|
-
<meta charset="UTF-8">
|
|
8
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
-
<title>Portals MCP — Authenticate</title>
|
|
10
|
-
<style>
|
|
11
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
-
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #0f172a; color: #e2e8f0; }
|
|
13
|
-
.card { background: #1e293b; border-radius: 12px; padding: 2rem; width: 100%; max-width: 400px; box-shadow: 0 4px 24px rgba(0,0,0,.4); }
|
|
14
|
-
h1 { font-size: 1.25rem; margin-bottom: 1.5rem; text-align: center; }
|
|
15
|
-
label { display: block; font-size: 0.875rem; margin-bottom: 0.5rem; color: #94a3b8; }
|
|
16
|
-
input { width: 100%; padding: 0.625rem 0.75rem; border-radius: 6px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: 1rem; outline: none; }
|
|
17
|
-
input:focus { border-color: #6366f1; }
|
|
18
|
-
button { width: 100%; margin-top: 1rem; padding: 0.625rem; border: none; border-radius: 6px; background: #6366f1; color: #fff; font-size: 1rem; cursor: pointer; }
|
|
19
|
-
button:hover { background: #4f46e5; }
|
|
20
|
-
.msg { margin-top: 1rem; text-align: center; font-size: 0.875rem; }
|
|
21
|
-
.msg.error { color: #f87171; }
|
|
22
|
-
.msg.success { color: #4ade80; }
|
|
23
|
-
</style>
|
|
24
|
-
</head>
|
|
25
|
-
<body>
|
|
26
|
-
<div class="card">
|
|
27
|
-
<h1>Portals MCP — Authenticate</h1>
|
|
28
|
-
<form id="form">
|
|
29
|
-
<label for="key">Access Key</label>
|
|
30
|
-
<input id="key" type="password" placeholder="Paste your access key" autofocus required />
|
|
31
|
-
<button type="submit">Authenticate</button>
|
|
32
|
-
</form>
|
|
33
|
-
<div id="msg" class="msg"></div>
|
|
34
|
-
</div>
|
|
35
|
-
<script>
|
|
36
|
-
const form = document.getElementById("form");
|
|
37
|
-
const msg = document.getElementById("msg");
|
|
38
|
-
form.addEventListener("submit", async (e) => {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
msg.textContent = "";
|
|
41
|
-
msg.className = "msg";
|
|
42
|
-
const key = document.getElementById("key").value.trim();
|
|
43
|
-
if (!key) return;
|
|
44
|
-
try {
|
|
45
|
-
const res = await fetch("/callback", {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: { "Content-Type": "application/json" },
|
|
48
|
-
body: JSON.stringify({ accessKey: key }),
|
|
49
|
-
});
|
|
50
|
-
const data = await res.json();
|
|
51
|
-
if (res.ok) {
|
|
52
|
-
msg.textContent = data.message;
|
|
53
|
-
msg.className = "msg success";
|
|
54
|
-
form.querySelector("button").disabled = true;
|
|
55
|
-
form.querySelector("input").disabled = true;
|
|
56
|
-
} else {
|
|
57
|
-
msg.textContent = data.message;
|
|
58
|
-
msg.className = "msg error";
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
msg.textContent = "Could not reach the server.";
|
|
62
|
-
msg.className = "msg error";
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
</script>
|
|
66
|
-
</body>
|
|
67
|
-
</html>`;
|
|
4
|
+
const PORTAL_URL = "https://theportal.to/mcp";
|
|
68
5
|
export function authenticateViaBrowser() {
|
|
69
6
|
return new Promise((resolve, reject) => {
|
|
70
7
|
const httpServer = createServer(async (req, res) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
8
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
9
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
10
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
11
|
+
if (req.method === "OPTIONS") {
|
|
12
|
+
res.writeHead(204);
|
|
13
|
+
res.end();
|
|
74
14
|
return;
|
|
75
15
|
}
|
|
76
16
|
if (req.method === "POST" && req.url === "/callback") {
|
|
@@ -87,7 +27,7 @@ export function authenticateViaBrowser() {
|
|
|
87
27
|
resolve({ uid, accessKey });
|
|
88
28
|
}
|
|
89
29
|
catch (err) {
|
|
90
|
-
const message = err instanceof Error ? err.message : "Invalid access key.
|
|
30
|
+
const message = err instanceof Error ? err.message : "Invalid access key.";
|
|
91
31
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
92
32
|
res.end(JSON.stringify({ message }));
|
|
93
33
|
}
|
|
@@ -106,8 +46,8 @@ export function authenticateViaBrowser() {
|
|
|
106
46
|
reject(new Error("Failed to start auth server"));
|
|
107
47
|
return;
|
|
108
48
|
}
|
|
109
|
-
const
|
|
110
|
-
exec(`open "${
|
|
49
|
+
const callbackPort = addr.port;
|
|
50
|
+
exec(`open "${PORTAL_URL}?callbackPort=${callbackPort}"`);
|
|
111
51
|
});
|
|
112
52
|
});
|
|
113
53
|
}
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile, writeFile, mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { uploadRoomData, setAccessKey, downloadRoomData } from "./api.js";
|
|
3
6
|
import { authenticateViaBrowser } from "./auth.js";
|
|
4
7
|
export function registerTools(server) {
|
|
5
8
|
// ── Auth ──
|
|
@@ -15,75 +18,24 @@ export function registerTools(server) {
|
|
|
15
18
|
return { content: [{ type: "text", text: "Authentication failed." }], isError: true };
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
|
-
server.registerTool("list_my_rooms", {
|
|
19
|
-
description: "List all room IDs where you are an owner or admin",
|
|
20
|
-
}, async () => {
|
|
21
|
-
const rooms = await getUserRooms();
|
|
22
|
-
return { content: [{ type: "text", text: JSON.stringify(rooms, null, 2) }] };
|
|
23
|
-
});
|
|
24
21
|
// ── Read tools ──
|
|
25
|
-
server.registerTool("
|
|
26
|
-
description: "
|
|
27
|
-
inputSchema: { roomId: z.string() },
|
|
28
|
-
}, async ({ roomId }) => {
|
|
29
|
-
const data = await getRoomData(roomId);
|
|
30
|
-
return { content: [{ type: "text", text: JSON.stringify(data.roomItems, null, 2) }] };
|
|
31
|
-
});
|
|
32
|
-
server.registerTool("get_room_settings", {
|
|
33
|
-
description: "Get room settings",
|
|
34
|
-
inputSchema: { roomId: z.string() },
|
|
35
|
-
}, async ({ roomId }) => {
|
|
36
|
-
const data = await getRoomData(roomId);
|
|
37
|
-
return { content: [{ type: "text", text: JSON.stringify(data.settings, null, 2) }] };
|
|
38
|
-
});
|
|
39
|
-
server.registerTool("get_room_tasks", {
|
|
40
|
-
description: "Get room tasks",
|
|
41
|
-
inputSchema: { roomId: z.string() },
|
|
42
|
-
}, async ({ roomId }) => {
|
|
43
|
-
const data = await getRoomData(roomId);
|
|
44
|
-
return { content: [{ type: "text", text: JSON.stringify(data.roomTasks, null, 2) }] };
|
|
45
|
-
});
|
|
46
|
-
server.registerTool("get_room_quests", {
|
|
47
|
-
description: "Get quest definitions for a room",
|
|
22
|
+
server.registerTool("get_room_data", {
|
|
23
|
+
description: "Download all room data to a temporary JSON file and return the file path. Use the file path to read the data.",
|
|
48
24
|
inputSchema: { roomId: z.string() },
|
|
49
25
|
}, async ({ roomId }) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
inputSchema: { roomId: z.string() },
|
|
56
|
-
}, async ({ roomId }) => {
|
|
57
|
-
const data = await getRoomData(roomId);
|
|
58
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
26
|
+
const buffer = await downloadRoomData(roomId);
|
|
27
|
+
const dir = await mkdtemp(join(tmpdir(), "portals-"));
|
|
28
|
+
const filePath = join(dir, `room-data-${roomId}.json`);
|
|
29
|
+
await writeFile(filePath, buffer);
|
|
30
|
+
return { content: [{ type: "text", text: `Room data saved to: ${filePath}` }] };
|
|
59
31
|
});
|
|
60
32
|
// ── Write tools ──
|
|
61
|
-
server.registerTool("
|
|
62
|
-
description: "Replace the entire room
|
|
63
|
-
inputSchema: { roomId: z.string(),
|
|
64
|
-
}, async ({ roomId,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
server.registerTool("set_room_settings", {
|
|
69
|
-
description: "Replace the entire room settings object",
|
|
70
|
-
inputSchema: { roomId: z.string(), data: z.any() },
|
|
71
|
-
}, async ({ roomId, data }) => {
|
|
72
|
-
await setRoomData(roomId, { settings: data });
|
|
73
|
-
return { content: [{ type: "text", text: "OK" }] };
|
|
74
|
-
});
|
|
75
|
-
server.registerTool("set_room_tasks", {
|
|
76
|
-
description: "Replace the entire room tasks object",
|
|
77
|
-
inputSchema: { roomId: z.string(), data: z.any() },
|
|
78
|
-
}, async ({ roomId, data }) => {
|
|
79
|
-
await setRoomData(roomId, { roomTasks: data });
|
|
80
|
-
return { content: [{ type: "text", text: "OK" }] };
|
|
81
|
-
});
|
|
82
|
-
server.registerTool("set_room_quests", {
|
|
83
|
-
description: "Replace the entire quests object",
|
|
84
|
-
inputSchema: { roomId: z.string(), data: z.any() },
|
|
85
|
-
}, async ({ roomId, data }) => {
|
|
86
|
-
await setRoomData(roomId, { quests: data });
|
|
33
|
+
server.registerTool("set_room_data", {
|
|
34
|
+
description: "Replace the entire room data. Provide an absolute path to a JSON file. JSON structure should be like : { roomItems: {...}, settings: {...}, roomTasks: {...}, quests: {...} }",
|
|
35
|
+
inputSchema: { roomId: z.string(), filePath: z.string() },
|
|
36
|
+
}, async ({ roomId, filePath }) => {
|
|
37
|
+
const data = await readFile(filePath);
|
|
38
|
+
await uploadRoomData(roomId, data);
|
|
87
39
|
return { content: [{ type: "text", text: "OK" }] };
|
|
88
40
|
});
|
|
89
41
|
}
|