vcluster-yaml-mcp-server 1.0.0 → 1.0.1

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
@@ -71,7 +71,7 @@ The package also provides a standalone CLI for quick queries and validation with
71
71
 
72
72
  ```bash
73
73
  # Quick start with npx (no installation)
74
- npx -y vcluster-yaml-mcp-server vcluster-yaml query sync --format table
74
+ npx -p vcluster-yaml-mcp-server vcluster-yaml query sync --format table
75
75
 
76
76
  # Or install globally
77
77
  npm install -g vcluster-yaml-mcp-server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vcluster-yaml-mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "MCP server for querying vcluster YAML configurations using jq",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -32,6 +32,7 @@
32
32
  "cli-table3": "^0.6.5",
33
33
  "commander": "^13.0.0",
34
34
  "express": "^4.18.2",
35
+ "express-rate-limit": "^8.1.0",
35
36
  "js-yaml": "^4.1.0",
36
37
  "node-fetch": "^3.3.2",
37
38
  "node-jq": "^6.0.1"
@@ -46,7 +47,6 @@
46
47
  "jsonschema": "^1.5.0",
47
48
  "strip-ansi": "^7.1.0",
48
49
  "supertest": "^7.1.4",
49
- "vitest": "^3.2.4",
50
- "z-schema": "^6.0.2"
50
+ "vitest": "^3.2.4"
51
51
  }
52
52
  }
package/src/github.js CHANGED
@@ -75,6 +75,16 @@ class GitHubClient {
75
75
 
76
76
  // Get file content from GitHub
77
77
  async getFileContent(path, ref = 'main') {
78
+ // Validate path - prevent path traversal
79
+ if (path.includes('..') || path.startsWith('/')) {
80
+ throw new Error('Invalid path: path traversal not allowed');
81
+ }
82
+
83
+ // Validate ref format (branch, tag, or commit SHA)
84
+ if (!/^[\w.\/-]+$/.test(ref)) {
85
+ throw new Error('Invalid ref format');
86
+ }
87
+
78
88
  const actualRef = ref;
79
89
  const cacheKey = `file:${actualRef}:${path}`;
80
90
  const cached = this.getFromCache(cacheKey);
@@ -3,16 +3,48 @@
3
3
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
4
  import { createServer } from './server.js';
5
5
  import express from 'express';
6
+ import rateLimit from 'express-rate-limit';
6
7
  import { requireApiKey } from './middleware/auth.js';
7
8
 
8
9
  const PORT = process.env.PORT || 3000;
9
10
  const REQUIRE_AUTH = process.env.REQUIRE_AUTH === 'true';
10
11
 
11
12
  const app = express();
12
- app.use(express.json());
13
+
14
+ // Security headers
15
+ app.use((req, res, next) => {
16
+ res.setHeader('X-Content-Type-Options', 'nosniff');
17
+ res.setHeader('X-Frame-Options', 'DENY');
18
+ res.removeHeader('X-Powered-By');
19
+ next();
20
+ });
21
+
22
+ // Request size limiting
23
+ app.use(express.json({
24
+ limit: '1mb',
25
+ strict: true
26
+ }));
27
+
28
+ // Rate limiting for general endpoints
29
+ const apiLimiter = rateLimit({
30
+ windowMs: 15 * 60 * 1000, // 15 minutes
31
+ max: 500,
32
+ message: { error: 'Too many requests, please try again later' },
33
+ standardHeaders: true,
34
+ legacyHeaders: false
35
+ });
36
+
37
+ // Rate limiting for MCP endpoint (allows for interactive sessions)
38
+ const mcpLimiter = rateLimit({
39
+ windowMs: 15 * 60 * 1000, // 15 minutes
40
+ max: 1000,
41
+ message: { error: 'Too many requests to MCP endpoint, please try again later' },
42
+ standardHeaders: true,
43
+ legacyHeaders: false
44
+ });
13
45
 
14
46
  // Health check endpoint
15
- app.get('/health', (_req, res) => {
47
+ app.get('/health', apiLimiter, (_req, res) => {
16
48
  res.json({
17
49
  status: 'ok',
18
50
  name: 'vcluster-yaml-mcp-server',
@@ -56,8 +88,8 @@ const mcpHandler = async (req, res) => {
56
88
  };
57
89
 
58
90
  // Support both GET and POST for MCP endpoint
59
- app.get('/mcp', REQUIRE_AUTH ? requireApiKey : (req, res, next) => next(), mcpHandler);
60
- app.post('/mcp', REQUIRE_AUTH ? requireApiKey : (req, res, next) => next(), mcpHandler);
91
+ app.get('/mcp', mcpLimiter, REQUIRE_AUTH ? requireApiKey : (req, res, next) => next(), mcpHandler);
92
+ app.post('/mcp', mcpLimiter, REQUIRE_AUTH ? requireApiKey : (req, res, next) => next(), mcpHandler);
61
93
 
62
94
  app.listen(PORT, () => {
63
95
  console.log(`vcluster-yaml-mcp-server HTTP running on port ${PORT}`);
@@ -1,4 +1,16 @@
1
- // Simple API key authentication middleware
1
+ import crypto from 'crypto';
2
+
3
+ // Timing-safe string comparison
4
+ function secureCompare(a, b) {
5
+ if (!a || !b || a.length !== b.length) return false;
6
+ try {
7
+ return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ // API key authentication middleware with timing-safe comparison
2
14
  export function requireApiKey(req, res, next) {
3
15
  const authHeader = req.headers['authorization'];
4
16
 
@@ -10,9 +22,14 @@ export function requireApiKey(req, res, next) {
10
22
  }
11
23
 
12
24
  const token = authHeader.substring(7);
13
- const validTokens = (process.env.VALID_API_KEYS || '').split(',');
25
+ const validTokens = (process.env.VALID_API_KEYS || '').split(',').filter(t => t.length > 0);
26
+
27
+ // Use timing-safe comparison to prevent timing attacks
28
+ const isValid = validTokens.some(validToken =>
29
+ validToken.length === token.length && secureCompare(validToken, token)
30
+ );
14
31
 
15
- if (!validTokens.includes(token)) {
32
+ if (!isValid) {
16
33
  return res.status(403).json({
17
34
  error: 'Forbidden',
18
35
  message: 'Invalid API key'
package/src/server.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
3
  import yaml from 'js-yaml';
4
- import jq from 'node-jq';
5
4
  import { githubClient } from './github.js';
6
5
  import { validateSnippet } from './snippet-validator.js';
7
6
 
@@ -591,77 +590,6 @@ export function createServer() {
591
590
  };
592
591
  }
593
592
 
594
- case 'query-config': {
595
- const version = args.version || 'main';
596
- let yamlData;
597
-
598
- // Load YAML data from GitHub or content
599
- if (args.content) {
600
- yamlData = yaml.load(args.content);
601
- } else if (args.file) {
602
- yamlData = await githubClient.getYamlContent(args.file, version);
603
- } else {
604
- // Default to chart/values.yaml
605
- yamlData = await githubClient.getYamlContent('chart/values.yaml', version);
606
- }
607
-
608
- // Convert YAML to JSON for jq processing
609
- const jsonData = JSON.stringify(yamlData);
610
-
611
- // Run jq query
612
- const options = {
613
- input: 'string',
614
- output: args.raw ? 'string' : 'json'
615
- };
616
-
617
- const result = await jq.run(args.query, jsonData, options);
618
-
619
- return {
620
- content: [
621
- {
622
- type: 'text',
623
- text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
624
- }
625
- ]
626
- };
627
- }
628
-
629
- case 'get-config-value': {
630
- const version = args.version || 'main';
631
- let yamlData;
632
-
633
- // Load YAML data from GitHub or content
634
- if (args.content) {
635
- yamlData = yaml.load(args.content);
636
- } else if (args.file) {
637
- yamlData = await githubClient.getYamlContent(args.file, version);
638
- } else {
639
- yamlData = await githubClient.getYamlContent('config/values.yaml', version);
640
- }
641
-
642
- // Convert dot notation to jq query
643
- const jqQuery = '.' + args.path.split('.').map(part => {
644
- // Handle array indices
645
- if (/^\d+$/.test(part)) {
646
- return `[${part}]`;
647
- }
648
- // Handle special characters in keys
649
- return `["${part}"]`;
650
- }).join('');
651
-
652
- const jsonData = JSON.stringify(yamlData);
653
- const result = await jq.run(jqQuery, jsonData, { input: 'string', output: 'json' });
654
-
655
- return {
656
- content: [
657
- {
658
- type: 'text',
659
- text: `Value at ${args.path}: ${JSON.stringify(result, null, 2)}`
660
- }
661
- ]
662
- };
663
- }
664
-
665
593
  case 'extract-validation-rules': {
666
594
  const version = args.version || 'main';
667
595
  const fileName = args.file || 'chart/values.yaml';
@@ -756,67 +684,6 @@ export function createServer() {
756
684
  }
757
685
  }
758
686
 
759
- case 'search-config': {
760
- const version = args.version || 'main';
761
- let yamlData;
762
-
763
- // Load YAML data from GitHub or content
764
- if (args.content) {
765
- yamlData = yaml.load(args.content);
766
- } else if (args.file) {
767
- yamlData = await githubClient.getYamlContent(args.file, version);
768
- } else {
769
- yamlData = await githubClient.getYamlContent('config/values.yaml', version);
770
- }
771
-
772
- const searchTerm = args.search.toLowerCase();
773
- const matches = [];
774
-
775
- // Recursive search function
776
- function searchObject(obj, path = '') {
777
- if (obj && typeof obj === 'object') {
778
- for (const [key, value] of Object.entries(obj)) {
779
- const currentPath = path ? `${path}.${key}` : key;
780
-
781
- // Check if key matches
782
- if (key.toLowerCase().includes(searchTerm)) {
783
- if (args.keysOnly) {
784
- matches.push(`Key: ${currentPath}`);
785
- } else {
786
- matches.push(`Key: ${currentPath} = ${JSON.stringify(value)}`);
787
- }
788
- }
789
-
790
- // Check if value matches (if not keysOnly)
791
- if (!args.keysOnly && value !== null && value !== undefined) {
792
- const valueStr = JSON.stringify(value).toLowerCase();
793
- if (valueStr.includes(searchTerm)) {
794
- matches.push(`Value at ${currentPath}: ${JSON.stringify(value)}`);
795
- }
796
- }
797
-
798
- // Recurse into objects
799
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
800
- searchObject(value, currentPath);
801
- }
802
- }
803
- }
804
- }
805
-
806
- searchObject(yamlData);
807
-
808
- return {
809
- content: [
810
- {
811
- type: 'text',
812
- text: matches.length > 0
813
- ? `Found ${matches.length} match(es):\n${matches.join('\n')}`
814
- : `No matches found for "${args.search}"`
815
- }
816
- ]
817
- };
818
- }
819
-
820
687
  default:
821
688
  throw new Error(`Unknown tool: ${name}`);
822
689
  }
@@ -190,6 +190,18 @@ function createAjvInstance() {
190
190
  */
191
191
  export function validateSnippet(snippetYaml, fullSchema, version, sectionHint = null) {
192
192
  const startTime = Date.now();
193
+ const MAX_YAML_SIZE = 1024 * 1024; // 1MB
194
+
195
+ // Check YAML size limit
196
+ if (snippetYaml.length > MAX_YAML_SIZE) {
197
+ return {
198
+ valid: false,
199
+ error: 'YAML exceeds 1MB limit',
200
+ size_bytes: snippetYaml.length,
201
+ max_size_bytes: MAX_YAML_SIZE,
202
+ elapsed_ms: Date.now() - startTime
203
+ };
204
+ }
193
205
 
194
206
  // Parse YAML
195
207
  let parsedSnippet;