@vibeinfra/mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/package.json +54 -0
- package/server.json +33 -0
- package/src/api.js +76 -0
- package/src/cli.js +15 -0
- package/src/server.js +54 -0
- package/src/tools.js +190 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VibeInfra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @vibeinfra/mcp-server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for [VibeInfra](https://vibeinfra.id) — lets an AI agent browse
|
|
4
|
+
the SRE incident catalog, read one incident's full blueprint, check live sandbox capacity and
|
|
5
|
+
look up infrastructure terminology.
|
|
6
|
+
|
|
7
|
+
Every tool is **read-only** and **unauthenticated**: the VibeInfra catalog is public by design,
|
|
8
|
+
so an agent can explore it with no credentials and no side effects.
|
|
9
|
+
|
|
10
|
+
## Use it
|
|
11
|
+
|
|
12
|
+
No install step — run it straight from npm:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y @vibeinfra/mcp-server
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Claude Code
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
claude mcp add vibeinfra -- npx -y @vibeinfra/mcp-server
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Claude Desktop, or any client with a JSON config
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"vibeinfra": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "@vibeinfra/mcp-server"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The server speaks stdio and requires Node 18+.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| Tool | What it does |
|
|
42
|
+
| ---------------------------- | ---------------------------------------------------------------------------- |
|
|
43
|
+
| `list_incidents` | Every incident and course in the catalog, with tier, stack, duration and link. |
|
|
44
|
+
| `search_incidents` | Free-text search plus filters for difficulty, category, domain, tech, free-only. |
|
|
45
|
+
| `get_incident` | One full blueprint by catalog ID, including objectives and chapters. |
|
|
46
|
+
| `get_platform_capacity` | Live sandbox usage and queue length. |
|
|
47
|
+
| `lookup_infrastructure_term` | The VibeInfra infrastructure/SRE glossary. |
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
| Environment variable | Default | Purpose |
|
|
52
|
+
| ------------------------ | -------------------------- | ------------------------------------ |
|
|
53
|
+
| `VIBEINFRA_API_BASE_URL` | `https://api.vibeinfra.id` | Point the server at a local backend. |
|
|
54
|
+
|
|
55
|
+
## Try it
|
|
56
|
+
|
|
57
|
+
Ask your agent:
|
|
58
|
+
|
|
59
|
+
> Find me an expert-tier Kafka incident on VibeInfra and summarise what it teaches.
|
|
60
|
+
|
|
61
|
+
> Is VibeInfra busy right now, or can I start a lab straight away?
|
|
62
|
+
|
|
63
|
+
## Related
|
|
64
|
+
|
|
65
|
+
- Server card — <https://vibeinfra.id/.well-known/mcp.json>
|
|
66
|
+
- Docs — <https://vibeinfra.id/docs/mcp/>
|
|
67
|
+
- REST API and OpenAPI description — <https://vibeinfra.id/openapi.json>
|
|
68
|
+
- JavaScript SDK (`@vibeinfra/sdk`) and Python SDK (`vibeinfra`) — <https://vibeinfra.id/developers/>
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install
|
|
74
|
+
npm test
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibeinfra/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol server for VibeInfra — lets an AI agent browse the SRE incident catalog, read live sandbox capacity and look up infrastructure terms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"modelcontextprotocol",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"vibeinfra",
|
|
10
|
+
"sre",
|
|
11
|
+
"incident-response",
|
|
12
|
+
"infrastructure",
|
|
13
|
+
"devops",
|
|
14
|
+
"kubernetes"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://vibeinfra.id/docs/mcp/",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/ridwansswnto/vibeinfraid/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ridwansswnto/vibeinfraid.git",
|
|
23
|
+
"directory": "packages/mcp-server"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "VibeInfra",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./src/server.js",
|
|
29
|
+
"bin": {
|
|
30
|
+
"vibeinfra-mcp": "src/cli.js",
|
|
31
|
+
"mcp-server": "src/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"server.json",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"start": "node src/cli.js",
|
|
44
|
+
"test": "node --test test/*.test.js"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
48
|
+
"zod": "^3.25.76"
|
|
49
|
+
},
|
|
50
|
+
"mcpName": "id.vibeinfra/catalog",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "id.vibeinfra/catalog",
|
|
4
|
+
"title": "VibeInfra Catalog",
|
|
5
|
+
"description": "Browse the VibeInfra SRE incident catalog, live sandbox capacity and infrastructure glossary.",
|
|
6
|
+
"websiteUrl": "https://vibeinfra.id/docs/mcp/",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/ridwansswnto/vibeinfraid",
|
|
9
|
+
"source": "github",
|
|
10
|
+
"subfolder": "packages/mcp-server"
|
|
11
|
+
},
|
|
12
|
+
"version": "0.1.0",
|
|
13
|
+
"packages": [
|
|
14
|
+
{
|
|
15
|
+
"registryType": "npm",
|
|
16
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
|
+
"identifier": "@vibeinfra/mcp-server",
|
|
18
|
+
"version": "0.1.0",
|
|
19
|
+
"transport": {
|
|
20
|
+
"type": "stdio"
|
|
21
|
+
},
|
|
22
|
+
"environmentVariables": [
|
|
23
|
+
{
|
|
24
|
+
"name": "VIBEINFRA_API_BASE_URL",
|
|
25
|
+
"description": "Override the API origin. Defaults to https://api.vibeinfra.id; set this only to point the server at a local backend.",
|
|
26
|
+
"isRequired": false,
|
|
27
|
+
"isSecret": false,
|
|
28
|
+
"default": "https://api.vibeinfra.id"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal client for the public VibeInfra API.
|
|
3
|
+
*
|
|
4
|
+
* This duplicates a slice of `@vibeinfra/sdk` on purpose: the MCP server is meant to be run
|
|
5
|
+
* with `npx -y @vibeinfra/mcp-server`, and every extra runtime dependency is another package
|
|
6
|
+
* an agent host has to resolve before the server can answer its first tool call.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_BASE_URL = "https://api.vibeinfra.id";
|
|
10
|
+
|
|
11
|
+
const API_VERSION = "v1";
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
13
|
+
|
|
14
|
+
/** The API answered with a non-2xx status. */
|
|
15
|
+
export class ApiError extends Error {
|
|
16
|
+
constructor(status, url, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ApiError";
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.url = url;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class VibeInfraApi {
|
|
25
|
+
constructor({ baseUrl = DEFAULT_BASE_URL, timeoutMs = DEFAULT_TIMEOUT_MS, fetch: fetchImpl } = {}) {
|
|
26
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
27
|
+
this.timeoutMs = timeoutMs;
|
|
28
|
+
this.fetch = fetchImpl ?? globalThis.fetch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async get(path, query) {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
34
|
+
if (value !== undefined && value !== null && value !== "") params.set(key, String(value));
|
|
35
|
+
}
|
|
36
|
+
const search = params.toString();
|
|
37
|
+
const url = `${this.baseUrl}/api/${API_VERSION}${path}${search ? `?${search}` : ""}`;
|
|
38
|
+
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
41
|
+
let response;
|
|
42
|
+
try {
|
|
43
|
+
response = await this.fetch(url, {
|
|
44
|
+
headers: { Accept: "application/json" },
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new ApiError(response.status, url, `GET ${url} failed with ${response.status}`);
|
|
53
|
+
}
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
listCourses(locale) {
|
|
58
|
+
return this.get("/courses", { locale });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getCourse(courseId, locale) {
|
|
62
|
+
return this.get(`/courses/${encodeURIComponent(courseId)}`, { locale });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getGlossary(lang) {
|
|
66
|
+
return this.get("/glossary", { lang });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getCapacity() {
|
|
70
|
+
return this.get("/labs/capacity");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getLeaderboard() {
|
|
74
|
+
return this.get("/labs/leaderboard");
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* stdio entrypoint: `npx -y @vibeinfra/mcp-server`.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is written to stdout except the protocol itself -- stdout is the transport, so a
|
|
6
|
+
* stray log line corrupts the stream. Diagnostics go to stderr.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
|
|
11
|
+
import { createServer } from "./server.js";
|
|
12
|
+
|
|
13
|
+
const server = createServer({ baseUrl: process.env.VIBEINFRA_API_BASE_URL });
|
|
14
|
+
|
|
15
|
+
await server.connect(new StdioServerTransport());
|
package/src/server.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the VibeInfra MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from `cli.js` so the server can be constructed and connected to an in-memory
|
|
5
|
+
* transport in tests, without a child process or a stdio pipe.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
|
|
10
|
+
import { VibeInfraApi } from "./api.js";
|
|
11
|
+
import { tools } from "./tools.js";
|
|
12
|
+
|
|
13
|
+
export const SERVER_NAME = "id.vibeinfra/catalog";
|
|
14
|
+
export const SERVER_VERSION = "0.1.0";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
* @param {string} [options.baseUrl] API origin. Defaults to production.
|
|
19
|
+
* @param {VibeInfraApi} [options.api] Pre-built API client, for tests.
|
|
20
|
+
*/
|
|
21
|
+
export function createServer(options = {}) {
|
|
22
|
+
const api = options.api ?? new VibeInfraApi({ baseUrl: options.baseUrl });
|
|
23
|
+
|
|
24
|
+
const server = new McpServer(
|
|
25
|
+
{ name: SERVER_NAME, title: "VibeInfra Catalog", version: SERVER_VERSION },
|
|
26
|
+
{
|
|
27
|
+
instructions:
|
|
28
|
+
"VibeInfra is a learn-by-doing infrastructure engineering platform: engineers " +
|
|
29
|
+
"troubleshoot realistic production incidents (Linux kernel, Docker, Kubernetes, Nginx, " +
|
|
30
|
+
"PostgreSQL, Kafka, AWS) in live disposable sandboxes with automated grading. Use these " +
|
|
31
|
+
"tools to find an incident that matches what someone wants to learn, to read its " +
|
|
32
|
+
"blueprint, and to check whether the platform has capacity right now. Every tool is " +
|
|
33
|
+
"read-only and needs no credentials.",
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
for (const tool of tools) {
|
|
38
|
+
// Errors are reported as tool results rather than thrown: a transport-level failure would
|
|
39
|
+
// end the client's turn, where an isError result lets the model see what went wrong (a
|
|
40
|
+
// typo'd course ID, the API being down) and try something else.
|
|
41
|
+
server.registerTool(tool.name, tool.config, async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
return await tool.handler(api, args);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
isError: true,
|
|
47
|
+
content: [{ type: "text", text: `${tool.name} failed: ${error.message}` }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return server;
|
|
54
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tools this MCP server exposes, defined independently of the transport.
|
|
3
|
+
*
|
|
4
|
+
* Each entry is a plain object with a name, a config and a handler taking `(api, args)`. That
|
|
5
|
+
* shape keeps the tools testable without standing up a stdio server, and keeps `server.js`
|
|
6
|
+
* down to wiring.
|
|
7
|
+
*
|
|
8
|
+
* Every tool is read-only and unauthenticated. The VibeInfra catalog is public by design --
|
|
9
|
+
* it is the platform's index -- so an agent can explore it with no credentials and no side
|
|
10
|
+
* effects, which is why each tool is annotated `readOnlyHint`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
const DIFFICULTIES = ["Beginner", "Intermediate", "Advanced", "Expert"];
|
|
16
|
+
|
|
17
|
+
/** Render a value as the JSON text content MCP clients expect. */
|
|
18
|
+
function jsonContent(value) {
|
|
19
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The fields worth showing in a list. The full blueprint is a `get_incident` call away. */
|
|
23
|
+
function summarise(course) {
|
|
24
|
+
return {
|
|
25
|
+
id: course.id,
|
|
26
|
+
title: course.title,
|
|
27
|
+
type: course.type,
|
|
28
|
+
category: course.category,
|
|
29
|
+
domain: course.domain,
|
|
30
|
+
difficulty: course.difficulty,
|
|
31
|
+
duration_minutes: course.duration_minutes,
|
|
32
|
+
xp_reward: course.xp_reward,
|
|
33
|
+
tech_stack: course.tech_stack,
|
|
34
|
+
status: course.status,
|
|
35
|
+
is_free: course.is_free,
|
|
36
|
+
description: course.description,
|
|
37
|
+
url: `https://vibeinfra.id/labs/overview/?course=${encodeURIComponent(course.id)}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Lower-cased searchable text for one course. */
|
|
42
|
+
function haystack(course) {
|
|
43
|
+
return [course.title, course.description, course.category, course.domain]
|
|
44
|
+
.concat(course.tech_stack ?? [])
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ")
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Apply the `search_incidents` filters. Shared with `list_incidents`, which is the no-query case. */
|
|
51
|
+
function filterCourses(courses, { query, difficulty, category, domain, tech, freeOnly } = {}) {
|
|
52
|
+
const needle = query?.trim().toLowerCase();
|
|
53
|
+
return courses.filter((course) => {
|
|
54
|
+
if (difficulty && course.difficulty !== difficulty) return false;
|
|
55
|
+
if (category && course.category?.toLowerCase() !== category.toLowerCase()) return false;
|
|
56
|
+
if (domain && course.domain?.toLowerCase() !== domain.toLowerCase()) return false;
|
|
57
|
+
if (freeOnly && !course.is_free) return false;
|
|
58
|
+
if (tech && !(course.tech_stack ?? []).some((t) => t.toLowerCase() === tech.toLowerCase())) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return !needle || haystack(course).includes(needle);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const tools = [
|
|
66
|
+
{
|
|
67
|
+
name: "list_incidents",
|
|
68
|
+
config: {
|
|
69
|
+
title: "List VibeInfra incidents",
|
|
70
|
+
description:
|
|
71
|
+
"List every incident simulation and course in the VibeInfra catalog. Each entry includes " +
|
|
72
|
+
"its difficulty tier, tech stack, expected duration and a link to its landing page. Use " +
|
|
73
|
+
"this to see what is available; use search_incidents to narrow by topic.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
locale: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("BCP 47 language tag, e.g. 'id'. Untranslated courses fall back to English."),
|
|
79
|
+
},
|
|
80
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
81
|
+
},
|
|
82
|
+
async handler(api, { locale } = {}) {
|
|
83
|
+
const courses = await api.listCourses(locale);
|
|
84
|
+
return jsonContent({ count: courses.length, incidents: courses.map(summarise) });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "search_incidents",
|
|
89
|
+
config: {
|
|
90
|
+
title: "Search VibeInfra incidents",
|
|
91
|
+
description:
|
|
92
|
+
"Find incident simulations by free-text query and/or by difficulty, category, domain, " +
|
|
93
|
+
"tech stack or free-to-run status. The query matches the title, description, category " +
|
|
94
|
+
"and tech stack. Returns the same summary shape as list_incidents.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
query: z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Free text, e.g. 'kafka consumer lag' or 'certificate expiry'."),
|
|
100
|
+
difficulty: z.enum(DIFFICULTIES).optional().describe("Exact difficulty tier."),
|
|
101
|
+
category: z.string().optional().describe("Catalog category, e.g. 'Kubernetes'."),
|
|
102
|
+
domain: z.string().optional().describe("Skill-ladder domain, e.g. 'Observability'."),
|
|
103
|
+
tech: z.string().optional().describe("A single tech-stack entry, e.g. 'PostgreSQL'."),
|
|
104
|
+
free_only: z.boolean().optional().describe("Only incidents that are free to run."),
|
|
105
|
+
locale: z.string().optional().describe("BCP 47 language tag."),
|
|
106
|
+
},
|
|
107
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
108
|
+
},
|
|
109
|
+
async handler(api, args = {}) {
|
|
110
|
+
const courses = await api.listCourses(args.locale);
|
|
111
|
+
const matches = filterCourses(courses, { ...args, freeOnly: args.free_only });
|
|
112
|
+
return jsonContent({
|
|
113
|
+
count: matches.length,
|
|
114
|
+
searched: courses.length,
|
|
115
|
+
incidents: matches.map(summarise),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "get_incident",
|
|
121
|
+
config: {
|
|
122
|
+
title: "Get one VibeInfra incident",
|
|
123
|
+
description:
|
|
124
|
+
"Fetch the full blueprint for one incident by catalog ID (for example " +
|
|
125
|
+
"'incident-22-k8s-crashloop'), including its learning objectives, chapters where the " +
|
|
126
|
+
"course is step-graded, tech stack and access tier. IDs come from list_incidents.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
course_id: z.string().describe("Catalog ID, e.g. 'incident-36-kafka-consumer-lag'."),
|
|
129
|
+
locale: z.string().optional().describe("BCP 47 language tag."),
|
|
130
|
+
},
|
|
131
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
132
|
+
},
|
|
133
|
+
async handler(api, { course_id: courseId, locale } = {}) {
|
|
134
|
+
const course = await api.getCourse(courseId, locale);
|
|
135
|
+
return jsonContent({
|
|
136
|
+
...course,
|
|
137
|
+
url: `https://vibeinfra.id/labs/overview/?course=${encodeURIComponent(courseId)}`,
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "get_platform_capacity",
|
|
143
|
+
config: {
|
|
144
|
+
title: "Get live sandbox capacity",
|
|
145
|
+
description:
|
|
146
|
+
"Read how much VibeInfra sandbox capacity is in use right now and how many learners are " +
|
|
147
|
+
"queued. Worth checking before telling a user to start a lab: when the queue is long, " +
|
|
148
|
+
"a start request is queued rather than provisioned immediately.",
|
|
149
|
+
inputSchema: {},
|
|
150
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
151
|
+
},
|
|
152
|
+
async handler(api) {
|
|
153
|
+
return jsonContent(await api.getCapacity());
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "lookup_infrastructure_term",
|
|
158
|
+
config: {
|
|
159
|
+
title: "Look up an infrastructure term",
|
|
160
|
+
description:
|
|
161
|
+
"Look up infrastructure and SRE terminology in the VibeInfra glossary — the same " +
|
|
162
|
+
"definitions used across the incident catalog. Omit the term to return the whole glossary.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
term: z
|
|
165
|
+
.string()
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Term to look up. Matched case-insensitively against labels and IDs."),
|
|
168
|
+
lang: z.string().optional().describe("Language code, e.g. 'id'."),
|
|
169
|
+
},
|
|
170
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
171
|
+
},
|
|
172
|
+
async handler(api, { term, lang } = {}) {
|
|
173
|
+
const entries = await api.getGlossary(lang);
|
|
174
|
+
if (!term) return jsonContent({ count: entries.length, entries });
|
|
175
|
+
const needle = term.trim().toLowerCase();
|
|
176
|
+
const exact = entries.filter(
|
|
177
|
+
(e) => e.label?.toLowerCase() === needle || e.id?.toLowerCase() === needle,
|
|
178
|
+
);
|
|
179
|
+
const matches = exact.length
|
|
180
|
+
? exact
|
|
181
|
+
: entries.filter(
|
|
182
|
+
(e) =>
|
|
183
|
+
e.label?.toLowerCase().includes(needle) || e.id?.toLowerCase().includes(needle),
|
|
184
|
+
);
|
|
185
|
+
return jsonContent({ term, count: matches.length, entries: matches });
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
export { filterCourses, summarise };
|