vcluster-yaml-mcp-server 1.0.7 → 1.0.9

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
@@ -1,5 +1,13 @@
1
1
  # vCluster YAML MCP Server
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/vcluster-yaml-mcp-server.svg)](https://www.npmjs.com/package/vcluster-yaml-mcp-server)
4
+ [![codecov](https://codecov.io/gh/Piotr1215/vcluster-yaml-mcp/branch/main/graph/badge.svg)](https://codecov.io/gh/Piotr1215/vcluster-yaml-mcp)
5
+ [![Node.js](https://img.shields.io/badge/node-18%20%7C%2020%20%7C%2022-brightgreen)](https://nodejs.org/)
6
+ [![Response Time](https://img.shields.io/badge/response-<100ms-brightgreen)](https://github.com/Piotr1215/vcluster-yaml-mcp-server#token-optimization)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Docker Pulls](https://img.shields.io/docker/pulls/piotrzan/vcluster-yaml-mcp-server.svg)](https://hub.docker.com/r/piotrzan/vcluster-yaml-mcp-server)
9
+ [![MCP](https://img.shields.io/badge/MCP-Server-blue.svg)](https://modelcontextprotocol.io)
10
+
3
11
  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
12
 
5
13
  ## What Does It Do?
@@ -71,11 +79,11 @@ The package also provides a standalone CLI for quick queries and validation with
71
79
 
72
80
  ```bash
73
81
  # Quick start with npx (no installation)
74
- npx -p vcluster-yaml-mcp-server vcluster-yaml query sync --format table
82
+ npx vcluster-yaml-mcp-server query sync
75
83
 
76
84
  # Or install globally
77
85
  npm install -g vcluster-yaml-mcp-server
78
- vcluster-yaml query sync --format table
86
+ vcluster-yaml query sync
79
87
 
80
88
  # Validate configurations with ease
81
89
  vcluster-yaml validate my-config.yaml
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "vcluster-yaml-mcp-server",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "MCP server for querying vcluster YAML configurations using jq",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
+ "vcluster-yaml-mcp-server": "./src/cli.js",
8
9
  "vcluster-yaml-mcp": "./src/index.js",
9
10
  "vcluster-yaml": "./src/cli.js"
10
11
  },
@@ -17,11 +18,17 @@
17
18
  "start:http": "node src/http-server.js",
18
19
  "docker:build": "docker build -t piotrzan/vcluster-yaml-mcp-server:latest .",
19
20
  "docker:push": "docker push piotrzan/vcluster-yaml-mcp-server:latest",
20
- "test": "vitest run",
21
- "test:watch": "vitest",
22
- "test:coverage": "vitest run --coverage",
21
+ "test": "vitest run --exclude tests/integration/**/*.test.js --exclude tests/ci/**/*.test.js --exclude tests/performance.test.js",
22
+ "test:unit": "vitest run --exclude tests/integration/**/*.test.js --exclude tests/ci/**/*.test.js --exclude tests/performance.test.js",
23
+ "test:integration": "vitest run tests/integration/",
23
24
  "test:perf": "vitest run tests/performance.test.js",
24
25
  "test:ci": "vitest run tests/ci/",
26
+ "test:all": "vitest run",
27
+ "test:watch": "vitest --exclude tests/integration/**/*.test.js",
28
+ "test:coverage": "vitest run --coverage --exclude tests/integration/**/*.test.js",
29
+ "test:mutation": "stryker run",
30
+ "complexity": "eslint --config eslint.config.complexity.js src/**/*.js",
31
+ "complexity:json": "eslint --config eslint.config.complexity.js --format json --output-file reports/complexity.json src/**/*.js || true",
25
32
  "lint:workflows": "actionlint .github/workflows/*.yml"
26
33
  },
27
34
  "dependencies": {
@@ -42,8 +49,12 @@
42
49
  "node": ">=18"
43
50
  },
44
51
  "devDependencies": {
52
+ "@stryker-mutator/core": "^9.2.0",
53
+ "@stryker-mutator/vitest-runner": "^9.2.0",
45
54
  "@vitest/coverage-v8": "^3.2.4",
55
+ "eslint": "^9.38.0",
46
56
  "execa": "^9.5.2",
57
+ "fast-check": "^4.3.0",
47
58
  "json-schema-library": "^10.3.0",
48
59
  "jsonschema": "^1.5.0",
49
60
  "strip-ansi": "^7.1.0",
package/src/cli.js CHANGED
@@ -35,12 +35,12 @@ program
35
35
  .description('Search for vCluster configuration fields')
36
36
  .option('--file <file>', 'Configuration file to search', 'chart/values.yaml')
37
37
  .option('-s, --schema-version <version>', 'vCluster version or branch', 'main')
38
- .option('-f, --format <format>', 'Output format (json, yaml, table)', 'json')
38
+ .option('-f, --format <format>', 'Output format: json, yaml, table (default: table)', 'table')
39
39
  .addHelpText('after', `
40
40
  Examples:
41
41
  $ vcluster-yaml query sync
42
42
  $ vcluster-yaml query sync --schema-version v0.24.0
43
- $ vcluster-yaml query "controlPlane.replicas" --format table
43
+ $ vcluster-yaml query "controlPlane.replicas"
44
44
  `)
45
45
  .action(async (query, options) => {
46
46
  try {
@@ -84,7 +84,7 @@ Examples:
84
84
  program
85
85
  .command('list-versions')
86
86
  .description('List available vCluster versions')
87
- .option('-f, --format <format>', 'Output format (json, yaml, table)', 'json')
87
+ .option('-f, --format <format>', 'Output format: json, yaml, table (default: table)', 'table')
88
88
  .action(async (options) => {
89
89
  try {
90
90
  // Validate format option
@@ -118,7 +118,7 @@ program
118
118
  .command('validate [file]')
119
119
  .description('Validate vCluster configuration')
120
120
  .option('-s, --schema-version <version>', 'vCluster version for schema', 'main')
121
- .option('-f, --format <format>', 'Output format (json, yaml, table)', 'json')
121
+ .option('-f, --format <format>', 'Output format: json, yaml, table (default: table)', 'table')
122
122
  .addHelpText('after', `
123
123
  Arguments:
124
124
  file YAML file to validate (use '-' for stdin, omit to read from stdin)
@@ -128,7 +128,6 @@ Examples:
128
128
  $ vcluster-yaml validate vcluster.yaml --schema-version v0.24.0
129
129
  $ cat vcluster.yaml | vcluster-yaml validate -
130
130
  $ vcluster-yaml validate - < vcluster.yaml
131
- $ vcluster-yaml validate vcluster.yaml --format table
132
131
  `)
133
132
  .action(async (file, options) => {
134
133
  try {
@@ -153,50 +153,79 @@ function validatePath(path, value, schema) {
153
153
  }
154
154
 
155
155
  /**
156
- * Find similar paths in schema for suggestions
156
+ * Find similar paths using priority-based matching (no magic numbers)
157
+ * Priority 1: Exact match on last segment (e.g., "version" matches "apiVersion")
158
+ * Priority 2: Substring match (e.g., "vers" matches "apiVersion")
159
+ * Priority 3: Shared prefix (e.g., "api.vers" matches "api.version")
157
160
  */
158
161
  function findSimilarPaths(targetPath, schema, maxSuggestions = 3) {
159
162
  const allPaths = extractSchemaPaths(schema);
163
+ if (allPaths.length === 0) return [];
164
+
160
165
  const targetParts = targetPath.toLowerCase().split('.');
161
166
  const targetLast = targetParts[targetParts.length - 1];
162
167
 
163
- const scored = allPaths.map(schemaPath => {
168
+ // Categorize matches by clear priorities (no arbitrary scoring)
169
+ const exactMatches = [];
170
+ const substringMatches = [];
171
+ const prefixMatches = [];
172
+
173
+ for (const schemaPath of allPaths) {
164
174
  const parts = schemaPath.toLowerCase().split('.');
165
175
  const last = parts[parts.length - 1];
166
176
 
167
- let score = 0;
168
- if (last === targetLast) score += 10;
169
- if (last.includes(targetLast) || targetLast.includes(last)) score += 5;
177
+ // Priority 1: Exact last segment match
178
+ if (last === targetLast) {
179
+ exactMatches.push(schemaPath);
180
+ continue;
181
+ }
182
+
183
+ // Priority 2: Substring match in last segment
184
+ if (last.includes(targetLast) || targetLast.includes(last)) {
185
+ substringMatches.push(schemaPath);
186
+ continue;
187
+ }
188
+
189
+ // Priority 3: Shared path prefix
190
+ const sharedPrefixLength = getSharedPrefix(targetParts, parts);
191
+ if (sharedPrefixLength > 0) {
192
+ prefixMatches.push({ path: schemaPath, prefixLength: sharedPrefixLength });
193
+ }
194
+ }
170
195
 
171
- // Shared prefix
172
- const sharedPrefix = getSharedPrefix(targetParts, parts);
173
- score += sharedPrefix * 2;
196
+ // Sort prefix matches by shared prefix length (longer is better)
197
+ prefixMatches.sort((a, b) => b.prefixLength - a.prefixLength);
174
198
 
175
- return { path: schemaPath, score };
176
- });
199
+ // Combine in strict priority order
200
+ const suggestions = [
201
+ ...exactMatches,
202
+ ...substringMatches,
203
+ ...prefixMatches.map(m => m.path)
204
+ ];
177
205
 
178
- return scored
179
- .filter(s => s.score > 0)
180
- .sort((a, b) => b.score - a.score)
181
- .slice(0, maxSuggestions)
182
- .map(s => s.path);
206
+ return suggestions.slice(0, maxSuggestions);
183
207
  }
184
208
 
185
209
  /**
186
210
  * Extract all paths from schema
211
+ * Contract: schema must be an object with optional .properties field
187
212
  */
188
213
  function extractSchemaPaths(schema, prefix = '') {
214
+ if (!schema || typeof schema !== 'object') {
215
+ return [];
216
+ }
217
+
189
218
  const paths = [];
219
+ // Explicit contract: use .properties if present, otherwise treat schema as properties object
190
220
  const props = schema.properties || schema;
191
221
 
192
- if (props && typeof props === 'object') {
193
- for (const [key, value] of Object.entries(props)) {
194
- const path = prefix ? `${prefix}.${key}` : key;
195
- paths.push(path);
222
+ for (const [key, value] of Object.entries(props)) {
223
+ const path = prefix ? `${prefix}.${key}` : key;
224
+ paths.push(path);
196
225
 
197
- if (value && value.properties) {
198
- paths.push(...extractSchemaPaths(value, path));
199
- }
226
+ // Recurse if nested properties exist
227
+ if (value && typeof value === 'object' && value.properties) {
228
+ paths.push(...extractSchemaPaths(value, path));
200
229
  }
201
230
  }
202
231