@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 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 & Coaching
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 the session ID and web terminal. |
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.2.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: process.env.VIBEINFRA_API_KEY,
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 };