vcluster-yaml-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/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # vCluster YAML MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that lets AI assistants query and validate [vCluster](https://github.com/loft-sh/vcluster) YAML configurations directly from GitHub.
4
+
5
+ ## What Does It Do?
6
+
7
+ This MCP server provides AI assistants with tools to:
8
+ - Query vCluster configuration options and schemas
9
+ - Validate YAML configurations
10
+ - Search for specific settings using natural language
11
+ - Query any version with explicit version parameters (stateless)
12
+ - Extract validation rules from comments
13
+
14
+ **Key feature:** No local files needed. All data is fetched live from the vCluster GitHub repository.
15
+
16
+ ## How It Works
17
+
18
+ The server uses the GitHub API to fetch vCluster YAML configurations, schemas, and documentation directly from the source:
19
+
20
+ 1. **GitHub as Source of Truth**: Queries `github.com/loft-sh/vcluster` repository
21
+ 2. **Stateless Version Queries**: Every tool accepts an optional `version` parameter (e.g., `v0.19.0`, `main`)
22
+ 3. **Parallel Version Support**: Query multiple versions simultaneously without state conflicts
23
+ 4. **Live Data**: Always fetches the latest configuration for the requested version
24
+ 5. **Smart Caching**: 15-minute in-memory cache to avoid overloading GitHub API
25
+
26
+ ```mermaid
27
+ graph LR
28
+ A[Claude/AI] --> B[MCP Server]
29
+ B --> C[GitHub API]
30
+ C --> D[vCluster Repository]
31
+ B --> E[Parse YAML/JSON]
32
+ E --> F[Return Structured Data]
33
+ F --> A
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ ### Option 1: Local (stdio)
39
+
40
+ Run the server locally via npx:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "vcluster-yaml": {
46
+ "command": "npx",
47
+ "args": ["-y", "vcluster-yaml-mcp-server"]
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Option 2: Remote (HTTP)
54
+
55
+ Use the public instance (always running latest version):
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "vcluster-yaml": {
61
+ "type": "http",
62
+ "url": "https://vcluster-yaml.cloudrumble.net/mcp"
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Command-Line Interface
69
+
70
+ The package also provides a standalone CLI for quick queries and validation without MCP setup:
71
+
72
+ ```bash
73
+ # Quick start with npx (no installation)
74
+ npx -y vcluster-yaml-mcp-server vcluster-yaml query sync --format table
75
+
76
+ # Or install globally
77
+ npm install -g vcluster-yaml-mcp-server
78
+ vcluster-yaml query sync --format table
79
+ ```
80
+
81
+ 📖 **[Full CLI Documentation →](docs/CLI.md)**
82
+
83
+ ## Available Tools
84
+
85
+ ### Version Discovery
86
+
87
+ **list-versions** - Browse all available vCluster versions
88
+ ```javascript
89
+ // Returns tags (releases) and branches
90
+ // Example output: v0.19.0, v0.20.0, main, etc.
91
+ ```
92
+
93
+ ### Configuration Queries
94
+
95
+ All query tools accept an optional `version` parameter (defaults to "main"):
96
+
97
+ **smart-query** - Universal search using dot notation or natural language
98
+ ```javascript
99
+ smart-query --query="controlPlane.ingress.enabled" --version="v0.19.0"
100
+ smart-query --query="namespace syncing" --version="main"
101
+ smart-query --query="etcd" // Defaults to "main"
102
+ ```
103
+
104
+ **Output Format (kubectl-style):**
105
+ ```
106
+ Found 4 matches for "replicas" in chart/values.yaml (v0.24.0)
107
+
108
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109
+
110
+ MATCH: controlPlane.statefulSet.highAvailability.replicas
111
+ TYPE: integer
112
+ VALUE: 1
113
+
114
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
115
+
116
+ MATCH: controlPlane.coredns.deployment.replicas
117
+ TYPE: integer
118
+ VALUE: 1
119
+
120
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
121
+
122
+ MATCH: controlPlane.statefulSet.highAvailability
123
+ TYPE: object
124
+
125
+ FIELDS:
126
+ replicas <integer>
127
+ value: 1
128
+
129
+ leaseDuration <integer>
130
+ value: 60
131
+
132
+ renewDeadline <integer>
133
+ value: 40
134
+
135
+ retryPeriod <integer>
136
+ value: 15
137
+
138
+ RELATED CONFIGS:
139
+ • controlPlane.statefulSet.resources - Resource limits for HA mode
140
+ • controlPlane.backingStore.etcd.deploy.statefulSet.highAvailability
141
+ ```
142
+
143
+ **Features:**
144
+ - ✅ **Structured output** - kubectl-style format
145
+ - ✅ **Type information** - Every value shows its type (integer, string, boolean, array, object)
146
+ - ✅ **Relevance ranking** - Exact matches appear first, results sorted by relevance
147
+ - ✅ **Related configs** - Suggests commonly configured fields together
148
+ - ✅ **Smart formatting** - Objects show structure, not full content dumps
149
+ - ✅ **LLM-friendly** - Easy to parse and understand for AI assistants
150
+
151
+ ### Config Creation & Validation
152
+
153
+ All validation tools accept an optional `version` parameter (defaults to "main"):
154
+
155
+ **create-vcluster-config** - Create and validate configs in one step (PRIMARY TOOL)
156
+ ```javascript
157
+ // Claude uses this when generating configs for you
158
+ // Ensures every generated config is validated before you see it
159
+ create-vcluster-config --yaml_content="<generated-yaml>" --description="Node sync config" --version="v0.24.0"
160
+
161
+ // Returns:
162
+ // ✅ Configuration validated successfully!
163
+ // Version: v0.24.0
164
+ // Section: sync
165
+ // Validation time: 45ms
166
+ //
167
+ // ### Configuration:
168
+ // [your YAML here]
169
+ ```
170
+
171
+ **validate-config** - Validate existing YAML configs
172
+ ```javascript
173
+ // Validate user-provided configs against specific version
174
+ validate-config --content="<your-yaml>" --version="v0.24.0"
175
+
176
+ // Validate files from GitHub
177
+ validate-config --file="chart/values.yaml" --version="main"
178
+
179
+ // Works with full configs or partial snippets (auto-detects section)
180
+ // Returns: { valid: true/false, errors: [...], section: "...", version: "...", elapsed_ms: <100 }
181
+ ```
182
+
183
+ **extract-validation-rules** - Get validation rules from YAML comments
184
+ ```javascript
185
+ extract-validation-rules --section="controlPlane" --version="v0.24.0"
186
+ // Returns: { rules, enums, dependencies, defaults }
187
+ // Extracts constraints like "Valid values: a, b, c"
188
+ ```
189
+
190
+ ## Usage Examples
191
+
192
+ ### Interactive Config Creation (Primary Workflow)
193
+
194
+ Ask Claude:
195
+ > "Create a vCluster config with node sync enabled and etcd embedded"
196
+
197
+ Claude will:
198
+ 1. Use `smart-query` or `extract-validation-rules` to research options
199
+ 2. Generate the YAML configuration
200
+ 3. **Automatically** call `create-vcluster-config` to validate
201
+ 4. Return validated, ready-to-use configuration
202
+
203
+ **Why this works:** The `create-vcluster-config` tool forces Claude to validate every config it generates. You'll always get validated configs.
204
+
205
+ ### Validate User-Provided Configuration
206
+
207
+ Ask Claude:
208
+ > "Is this ingress configuration valid for vCluster v0.24?"
209
+ > ```yaml
210
+ > ingress:
211
+ > enabled: true
212
+ > host: "my-vcluster.example.com"
213
+ > ```
214
+
215
+ Claude will:
216
+ 1. Use `validate-config` with `--version="v0.24.0"` parameter
217
+ 2. Report any validation errors with specific paths
218
+ 3. Suggest fixes if needed
219
+
220
+ ### Explore vCluster Options
221
+
222
+ Ask Claude:
223
+ > "What high availability options are available in vCluster v0.19.0?"
224
+
225
+ Claude will use:
226
+ - `smart-query` with `--version="v0.19.0"` to find HA-related settings
227
+ - No need to "switch" versions - query directly with version parameter
228
+
229
+ ### Compare Versions
230
+
231
+ Ask Claude:
232
+ > "How did the sync.fromHost configuration change between v0.19.0 and v0.20.0?"
233
+
234
+ Claude will use:
235
+ - `smart-query` with `--version="v0.19.0"` for first version
236
+ - `smart-query` with `--version="v0.20.0"` for second version
237
+ - Can query both versions in parallel (stateless design)
238
+
239
+ ## Token Optimization
240
+
241
+ This server is designed for efficient token usage with the new kubectl-style format:
242
+
243
+ | Tool | Tokens | Strategy | Performance |
244
+ |------|--------|----------|-------------|
245
+ | create-vcluster-config | ~300-600 | Validation + formatted response with emoji indicators | <100ms |
246
+ | validate-config | ~200-500 | Fast validation, precise errors only | <100ms |
247
+ | smart-query | ~800-1.5K | Structured output (was ~2K with JSON dumps), limits to 50 matches | <100ms |
248
+ | extract-validation-rules | ~2-5K | Section-specific filtering, cache for knowledge base | <100ms |
249
+
250
+ ## Development
251
+
252
+ ```bash
253
+ # Install dependencies
254
+ npm install
255
+
256
+ # Run locally (stdio)
257
+ node src/index.js
258
+
259
+ # Test with MCP Inspector
260
+ npx @modelcontextprotocol/inspector node src/index.js
261
+ # Open http://localhost:5173
262
+
263
+ # Run tests
264
+ npm test
265
+
266
+ # Run HTTP server locally
267
+ npm run start:http
268
+ # Server runs on http://localhost:3000
269
+ ```
270
+
271
+ ## Technical Details
272
+
273
+ - **SDK**: `@modelcontextprotocol/sdk` v1.20.1 (Streamable HTTP transport)
274
+ - **Node**: >=18
275
+ - **Transport**: Both stdio (local) and HTTP/SSE (remote)
276
+ - **Dependencies**: `js-yaml` for parsing, `node-jq` for querying, `node-fetch` for GitHub API
277
+
278
+ ## Release Process
279
+
280
+ This project uses automated CI/CD workflows for releases to npm, Docker Hub, and GitHub Releases.
281
+
282
+ 📖 **[Release Documentation →](docs/RELEASING.md)**
283
+
284
+ ## Links
285
+
286
+ - [vCluster GitHub](https://github.com/loft-sh/vcluster)
287
+ - [Model Context Protocol](https://modelcontextprotocol.io)
288
+ - [MCP Specification](https://spec.modelcontextprotocol.io)
289
+
290
+ ## License
291
+
292
+ MIT
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "vcluster-yaml-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for querying vcluster YAML configurations using jq",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "vcluster-yaml-mcp": "./src/index.js",
9
+ "vcluster-yaml": "./src/cli.js"
10
+ },
11
+ "files": [
12
+ "src/**/*.js",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node src/index.js",
17
+ "start:http": "node src/http-server.js",
18
+ "docker:build": "docker build -t piotrzan/vcluster-yaml-mcp-server:latest .",
19
+ "docker:push": "docker push piotrzan/vcluster-yaml-mcp-server:latest",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage",
23
+ "test:perf": "vitest run tests/performance.test.js",
24
+ "test:ci": "vitest run tests/ci/",
25
+ "lint:workflows": "actionlint .github/workflows/*.yml"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.20.1",
29
+ "ajv": "^8.17.1",
30
+ "ajv-formats": "^3.0.1",
31
+ "chalk": "^5.4.1",
32
+ "cli-table3": "^0.6.5",
33
+ "commander": "^13.0.0",
34
+ "express": "^4.18.2",
35
+ "js-yaml": "^4.1.0",
36
+ "node-fetch": "^3.3.2",
37
+ "node-jq": "^6.0.1"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "devDependencies": {
43
+ "@vitest/coverage-v8": "^3.2.4",
44
+ "execa": "^9.5.2",
45
+ "json-schema-library": "^10.3.0",
46
+ "jsonschema": "^1.5.0",
47
+ "strip-ansi": "^7.1.0",
48
+ "supertest": "^7.1.4",
49
+ "vitest": "^3.2.4",
50
+ "z-schema": "^6.0.2"
51
+ }
52
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * CLI handlers that bridge CLI commands to underlying logic
3
+ * Reuses githubClient and validation logic from server.js
4
+ * Returns structured data for CLI formatters
5
+ */
6
+
7
+ import yaml from 'js-yaml';
8
+ import { githubClient } from './github.js';
9
+ import { validateSnippet } from './snippet-validator.js';
10
+
11
+ // Helper function to get the type of a value
12
+ function getType(value) {
13
+ if (value === null) return 'null';
14
+ if (Array.isArray(value)) return 'array';
15
+ if (typeof value === 'object') return 'object';
16
+ if (typeof value === 'number') {
17
+ return Number.isInteger(value) ? 'integer' : 'number';
18
+ }
19
+ return typeof value;
20
+ }
21
+
22
+ // Helper function to rank search results
23
+ function rankResult(item, searchTerm) {
24
+ const pathLower = item.path.toLowerCase();
25
+ const keyLower = item.key.toLowerCase();
26
+ let score = 0;
27
+
28
+ // Exact match
29
+ if (pathLower === searchTerm || keyLower === searchTerm) {
30
+ score += 100;
31
+ }
32
+
33
+ // Starts with search term
34
+ if (pathLower.startsWith(searchTerm) || keyLower.startsWith(searchTerm)) {
35
+ score += 50;
36
+ }
37
+
38
+ // Ends with search term
39
+ if (pathLower.endsWith(searchTerm) || keyLower.endsWith(searchTerm)) {
40
+ score += 40;
41
+ }
42
+
43
+ // Contains search term
44
+ if (pathLower.includes(searchTerm) || keyLower.includes(searchTerm)) {
45
+ score += 30;
46
+ }
47
+
48
+ // Prefer leaf nodes (actual values)
49
+ if (item.isLeaf) {
50
+ score += 10;
51
+ }
52
+
53
+ // Prefer shorter paths (more specific)
54
+ const pathDepth = item.path.split('.').length;
55
+ score -= pathDepth;
56
+
57
+ return score;
58
+ }
59
+
60
+ /**
61
+ * Handle query command
62
+ * Searches for configuration fields in vCluster YAML
63
+ */
64
+ export async function handleQuery(query, options) {
65
+ const version = options.version || 'main';
66
+ const fileName = options.file || 'chart/values.yaml';
67
+
68
+ let yamlData;
69
+
70
+ try {
71
+ yamlData = await githubClient.getYamlContent(fileName, version);
72
+ } catch (error) {
73
+ return {
74
+ success: false,
75
+ error: `Could not load ${fileName} from GitHub (version: ${version}). Error: ${error.message}`,
76
+ metadata: {
77
+ query,
78
+ file: fileName,
79
+ version
80
+ }
81
+ };
82
+ }
83
+
84
+ const searchTerm = query.toLowerCase();
85
+ const results = [];
86
+
87
+ // Helper function to extract all paths and values
88
+ function extractInfo(obj, path = '') {
89
+ const info = [];
90
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
91
+ for (const [key, value] of Object.entries(obj)) {
92
+ const currentPath = path ? `${path}.${key}` : key;
93
+
94
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
95
+ info.push({ path: currentPath, key, value, isLeaf: false });
96
+ info.push(...extractInfo(value, currentPath));
97
+ } else {
98
+ info.push({ path: currentPath, key, value, isLeaf: true });
99
+ }
100
+ }
101
+ }
102
+ return info;
103
+ }
104
+
105
+ const allInfo = extractInfo(yamlData);
106
+
107
+ // Support dot notation queries
108
+ const isDotNotation = searchTerm.includes('.');
109
+
110
+ if (isDotNotation) {
111
+ // Exact and partial dot notation matching
112
+ for (const item of allInfo) {
113
+ const pathLower = item.path.toLowerCase();
114
+
115
+ if (pathLower === searchTerm ||
116
+ pathLower.endsWith(searchTerm) ||
117
+ pathLower.includes(searchTerm)) {
118
+ results.push(item);
119
+ }
120
+ }
121
+ } else {
122
+ // Keyword-based search
123
+ const keywords = searchTerm.split(/\s+/);
124
+
125
+ for (const item of allInfo) {
126
+ const pathLower = item.path.toLowerCase();
127
+ const keyLower = item.key.toLowerCase();
128
+ const valueStr = JSON.stringify(item.value).toLowerCase();
129
+
130
+ const allKeywordsMatch = keywords.every(kw =>
131
+ pathLower.includes(kw) || keyLower.includes(kw) || valueStr.includes(kw)
132
+ );
133
+
134
+ if (allKeywordsMatch) {
135
+ results.push(item);
136
+ }
137
+ }
138
+ }
139
+
140
+ // Sort results by relevance
141
+ results.sort((a, b) => {
142
+ const scoreA = rankResult(a, searchTerm);
143
+ const scoreB = rankResult(b, searchTerm);
144
+ return scoreB - scoreA;
145
+ });
146
+
147
+ // Format results for CLI output
148
+ const formattedResults = results.slice(0, 50).map(item => ({
149
+ field: item.path,
150
+ value: item.value,
151
+ type: getType(item.value),
152
+ path: item.path,
153
+ description: '' // Could be enhanced with comments parsing
154
+ }));
155
+
156
+ return {
157
+ success: true,
158
+ results: formattedResults,
159
+ metadata: {
160
+ query,
161
+ file: fileName,
162
+ version,
163
+ resultCount: formattedResults.length,
164
+ totalMatches: results.length
165
+ }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Handle list-versions command
171
+ * Lists available vCluster versions from GitHub
172
+ */
173
+ export async function handleListVersions() {
174
+ try {
175
+ const tags = await githubClient.getTags();
176
+
177
+ // Only show versions starting with 'v'
178
+ const versionTags = tags.filter(tag => tag.startsWith('v'));
179
+
180
+ // Always include main branch
181
+ const versions = ['main', ...versionTags];
182
+
183
+ return {
184
+ success: true,
185
+ versions,
186
+ metadata: {
187
+ totalCount: versions.length,
188
+ source: 'github'
189
+ }
190
+ };
191
+ } catch (error) {
192
+ return {
193
+ success: false,
194
+ error: `Failed to fetch versions: ${error.message}`,
195
+ versions: [],
196
+ metadata: {
197
+ totalCount: 0,
198
+ source: 'github'
199
+ }
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Handle validate command
206
+ * Validates vCluster YAML configuration
207
+ */
208
+ export async function handleValidate(content, options) {
209
+ const version = options.version || 'main';
210
+
211
+ // Validate YAML syntax first
212
+ let yamlData;
213
+ try {
214
+ yamlData = yaml.load(content);
215
+ } catch (error) {
216
+ return {
217
+ success: false,
218
+ valid: false,
219
+ errors: [
220
+ {
221
+ path: 'root',
222
+ message: error.message,
223
+ type: 'syntax'
224
+ }
225
+ ],
226
+ metadata: {
227
+ version,
228
+ contentLength: content.length
229
+ }
230
+ };
231
+ }
232
+
233
+ // Fetch schema for validation
234
+ try {
235
+ const schemaContent = await githubClient.getFileContent('chart/values.schema.json', version);
236
+ const fullSchema = JSON.parse(schemaContent);
237
+
238
+ // Use snippet validator
239
+ const result = validateSnippet(
240
+ content,
241
+ fullSchema,
242
+ version
243
+ );
244
+
245
+ if (result.valid) {
246
+ return {
247
+ success: true,
248
+ valid: true,
249
+ errors: [],
250
+ metadata: {
251
+ version,
252
+ contentLength: content.length
253
+ }
254
+ };
255
+ } else {
256
+ // Format errors for CLI
257
+ const errors = result.errors.map(err => ({
258
+ path: err.instancePath || err.dataPath || 'root',
259
+ message: err.message || 'Validation error',
260
+ type: err.keyword || 'validation'
261
+ }));
262
+
263
+ return {
264
+ success: true,
265
+ valid: false,
266
+ errors,
267
+ metadata: {
268
+ version,
269
+ contentLength: content.length
270
+ }
271
+ };
272
+ }
273
+ } catch (error) {
274
+ return {
275
+ success: false,
276
+ valid: false,
277
+ errors: [
278
+ {
279
+ path: 'root',
280
+ message: `Failed to load schema: ${error.message}`,
281
+ type: 'schema-error'
282
+ }
283
+ ],
284
+ metadata: {
285
+ version,
286
+ contentLength: content.length
287
+ }
288
+ };
289
+ }
290
+ }