@vibeinfra/mcp-server 0.2.0 → 0.3.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 +27 -2
- package/package.json +1 -1
- package/src/api.js +8 -0
- package/src/cli.js +25 -1
- package/src/login.js +142 -0
- package/src/tools.js +32 -0
package/README.md
CHANGED
|
@@ -48,16 +48,41 @@ The server speaks stdio and requires Node 18+.
|
|
|
48
48
|
| `get_platform_capacity` | Live sandbox usage and queue length. |
|
|
49
49
|
| `lookup_infrastructure_term` | The VibeInfra infrastructure/SRE glossary. |
|
|
50
50
|
|
|
51
|
-
### ⚡ Interactive SRE Incident Drills &
|
|
51
|
+
### ⚡ Interactive SRE Incident Drills, Diagnostics & Headless Troubleshooting
|
|
52
52
|
|
|
53
53
|
| Tool | What it does |
|
|
54
54
|
| ---------------------------- | ---------------------------------------------------------------------------- |
|
|
55
|
-
| `start_drill` | Spawns a live disposable sandbox and returns
|
|
55
|
+
| `start_drill` | Spawns a live disposable sandbox and returns session ID and web terminal. |
|
|
56
|
+
| `exec_in_sandbox` | Runs diagnostic/remediation commands (`kubectl`, `df`, `systemctl`) in sandbox. |
|
|
56
57
|
| `get_drill_hint` | Provides spoiler-free progressive guidance and diagnostic nudges. |
|
|
57
58
|
| `grade_drill` | Runs automated system-state evaluation against the live sandbox to score fixes. |
|
|
58
59
|
| `get_drill_status` | Returns real-time telemetry, node health status, and live topology. |
|
|
59
60
|
| `stop_drill` | Cleanly terminates and destroys the disposable sandbox container. |
|
|
60
61
|
|
|
62
|
+
## Authentication
|
|
63
|
+
|
|
64
|
+
### Zero-Config CLI Login (Recommended)
|
|
65
|
+
|
|
66
|
+
Authenticate your terminal or agent host in 5 seconds without touching JSON config files:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx -y @vibeinfra/mcp-server login
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This opens your browser to authorize your account and securely saves your Personal Access Token to `~/.vibeinfra/credentials.json` (POSIX `0600` permissions). Subsequent runs of `@vibeinfra/mcp-server` automatically discover this token.
|
|
73
|
+
|
|
74
|
+
To log out:
|
|
75
|
+
```bash
|
|
76
|
+
npx -y @vibeinfra/mcp-server logout
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 1-Click Dashboard Token
|
|
80
|
+
|
|
81
|
+
You can also generate a Personal Access Token from your VibeInfra Profile:
|
|
82
|
+
1. Navigate to **[https://vibeinfra.id/profile](https://vibeinfra.id/profile)**.
|
|
83
|
+
2. Under **AI Agents & MCP Integration**, click **Generate Agent Token**.
|
|
84
|
+
3. Click **Copy Configuration** and paste it directly into your `.mcp.json` or Cursor / Claude Code settings.
|
|
85
|
+
|
|
61
86
|
## Configuration
|
|
62
87
|
|
|
63
88
|
| Environment variable | Default | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeinfra/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Model Context Protocol server for VibeInfra — enables AI agents to browse the SRE incident catalog, start live disposable sandboxes, coach learners with hints, and grade remediations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
package/src/api.js
CHANGED
|
@@ -142,4 +142,12 @@ export class VibeInfraApi {
|
|
|
142
142
|
getLabTopology(sessionId) {
|
|
143
143
|
return this.get("/labs/topology", { session_id: sessionId });
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
execInSandbox(sessionId, command, timeoutSec = 15) {
|
|
147
|
+
return this.post("/labs/exec", {
|
|
148
|
+
session_id: sessionId,
|
|
149
|
+
command,
|
|
150
|
+
timeout_sec: timeoutSec,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
145
153
|
}
|
package/src/cli.js
CHANGED
|
@@ -9,10 +9,34 @@
|
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
|
|
11
11
|
import { createServer } from "./server.js";
|
|
12
|
+
import { getStoredApiKey, runLogin, runLogout } from "./login.js";
|
|
13
|
+
|
|
14
|
+
const command = process.argv[2];
|
|
15
|
+
|
|
16
|
+
if (command === "login") {
|
|
17
|
+
try {
|
|
18
|
+
await runLogin({
|
|
19
|
+
webBaseUrl: process.env.VIBEINFRA_WEB_URL || "https://vibeinfra.id",
|
|
20
|
+
});
|
|
21
|
+
process.exit(0);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error("❌ Login failed:", err.message || err);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (command === "logout") {
|
|
29
|
+
runLogout();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Automatically resolve API key from environment or ~/.vibeinfra/credentials.json
|
|
34
|
+
const resolvedApiKey = process.env.VIBEINFRA_API_KEY || getStoredApiKey();
|
|
12
35
|
|
|
13
36
|
const server = createServer({
|
|
14
37
|
baseUrl: process.env.VIBEINFRA_API_BASE_URL,
|
|
15
|
-
apiKey:
|
|
38
|
+
apiKey: resolvedApiKey,
|
|
16
39
|
});
|
|
17
40
|
|
|
18
41
|
await server.connect(new StdioServerTransport());
|
|
42
|
+
|
package/src/login.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { exec } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), ".vibeinfra");
|
|
9
|
+
const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read the stored API token from ~/.vibeinfra/credentials.json
|
|
13
|
+
*/
|
|
14
|
+
export function getStoredApiKey() {
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
17
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
18
|
+
if (data && data.api_key) {
|
|
19
|
+
return data.api_key;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Ignore unreadable or corrupted config
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Persist the API token with strict POSIX 0600 permissions
|
|
30
|
+
*/
|
|
31
|
+
export function saveApiKey(apiKey) {
|
|
32
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
33
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
CREDENTIALS_FILE,
|
|
37
|
+
JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
api_key: apiKey,
|
|
40
|
+
saved_at: new Date().toISOString(),
|
|
41
|
+
},
|
|
42
|
+
null,
|
|
43
|
+
2
|
|
44
|
+
),
|
|
45
|
+
{ mode: 0o600 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wipe stored credentials
|
|
51
|
+
*/
|
|
52
|
+
export function clearApiKey() {
|
|
53
|
+
try {
|
|
54
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
55
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openBrowser(url) {
|
|
63
|
+
const platform = os.platform();
|
|
64
|
+
if (platform === "darwin") {
|
|
65
|
+
exec(`open "${url}"`);
|
|
66
|
+
} else if (platform === "win32") {
|
|
67
|
+
exec(`start "" "${url}"`);
|
|
68
|
+
} else {
|
|
69
|
+
exec(`xdg-open "${url}"`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run interactive OAuth loopback flow
|
|
75
|
+
*/
|
|
76
|
+
export function runLogin({ webBaseUrl = "https://vibeinfra.id", port = 49152 } = {}) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const stateNonce = crypto.randomBytes(16).toString("hex");
|
|
79
|
+
|
|
80
|
+
const server = http.createServer((req, res) => {
|
|
81
|
+
const reqUrl = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
82
|
+
|
|
83
|
+
if (reqUrl.pathname === "/callback") {
|
|
84
|
+
const token = reqUrl.searchParams.get("token");
|
|
85
|
+
const state = reqUrl.searchParams.get("state");
|
|
86
|
+
|
|
87
|
+
if (!token || state !== stateNonce) {
|
|
88
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
89
|
+
res.end("<h3>Invalid authentication request or state mismatch.</h3>");
|
|
90
|
+
server.close();
|
|
91
|
+
return reject(new Error("State nonce mismatch or missing token"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
saveApiKey(token);
|
|
95
|
+
|
|
96
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
97
|
+
res.end(`
|
|
98
|
+
<!DOCTYPE html>
|
|
99
|
+
<html>
|
|
100
|
+
<head><title>VibeInfra CLI Authorized</title></head>
|
|
101
|
+
<body style="font-family: system-ui, -apple-system, sans-serif; background: #09090b; color: #fff; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
|
|
102
|
+
<div style="text-align: center; max-width: 440px; padding: 2rem; border-radius: 1rem; background: #18181b; border: 1px solid #27272a;">
|
|
103
|
+
<h2 style="color: #34d399; margin-top: 0;">✓ Successfully Authorized!</h2>
|
|
104
|
+
<p style="color: #a1a1aa; font-size: 0.95rem; line-height: 1.5;">
|
|
105
|
+
Your Personal Access Token has been saved securely to <code style="color: #e4e4e7; background: #27272a; padding: 2px 6px; border-radius: 4px;">~/.vibeinfra/credentials.json</code>.
|
|
106
|
+
</p>
|
|
107
|
+
<p style="color: #71717a; font-size: 0.85rem; margin-top: 1.5rem;">You can close this tab and return to your terminal.</p>
|
|
108
|
+
</div>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
111
|
+
`);
|
|
112
|
+
|
|
113
|
+
server.close();
|
|
114
|
+
console.error("\n✅ Successfully authenticated with VibeInfra!");
|
|
115
|
+
console.error(`Token securely saved to ${CREDENTIALS_FILE} (mode 0600).\n`);
|
|
116
|
+
return resolve();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
res.writeHead(404);
|
|
120
|
+
res.end("Not found");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
server.on("error", (err) => {
|
|
124
|
+
reject(err);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
server.listen(port, "127.0.0.1", () => {
|
|
128
|
+
const authUrl = `${webBaseUrl.replace(/\/+$/, "")}/auth/cli?port=${port}&state=${stateNonce}`;
|
|
129
|
+
console.error("\n🚀 Opening browser to authenticate with VibeInfra...");
|
|
130
|
+
console.error(`If the browser does not open automatically, visit:\n${authUrl}\n`);
|
|
131
|
+
openBrowser(authUrl);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle logout CLI command
|
|
138
|
+
*/
|
|
139
|
+
export function runLogout() {
|
|
140
|
+
clearApiKey();
|
|
141
|
+
console.error("\n👋 Successfully logged out. Removed credentials from ~/.vibeinfra/credentials.json\n");
|
|
142
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -320,6 +320,38 @@ export const tools = [
|
|
|
320
320
|
});
|
|
321
321
|
},
|
|
322
322
|
},
|
|
323
|
+
{
|
|
324
|
+
name: "exec_in_sandbox",
|
|
325
|
+
config: {
|
|
326
|
+
title: "Execute shell command inside incident sandbox",
|
|
327
|
+
description:
|
|
328
|
+
"Run an arbitrary diagnostic, triage, or remediation command directly inside the live container sandbox. " +
|
|
329
|
+
"Returns stdout, stderr, and exit_code. Enables headless incident troubleshooting without opening a browser.",
|
|
330
|
+
inputSchema: {
|
|
331
|
+
session_id: z.string().describe("Active session ID returned by start_drill."),
|
|
332
|
+
command: z
|
|
333
|
+
.string()
|
|
334
|
+
.describe("Shell command to execute, e.g. 'kubectl get pods -A', 'df -h', 'systemctl status nginx'."),
|
|
335
|
+
timeout_sec: z
|
|
336
|
+
.number()
|
|
337
|
+
.optional()
|
|
338
|
+
.describe("Command execution timeout in seconds (default 15, max 30)."),
|
|
339
|
+
},
|
|
340
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
341
|
+
},
|
|
342
|
+
async handler(api, { session_id: sessionId, command, timeout_sec: timeoutSec } = {}) {
|
|
343
|
+
const result = await api.execInSandbox(sessionId, command, timeoutSec);
|
|
344
|
+
return jsonContent({
|
|
345
|
+
session_id: sessionId,
|
|
346
|
+
command,
|
|
347
|
+
exit_code: result.exit_code ?? 0,
|
|
348
|
+
stdout: result.stdout ?? "",
|
|
349
|
+
stderr: result.stderr ?? "",
|
|
350
|
+
status: result.status ?? (result.exit_code === 0 ? "success" : "failed"),
|
|
351
|
+
error: result.error,
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
},
|
|
323
355
|
];
|
|
324
356
|
|
|
325
357
|
export { filterCourses, summarise };
|