vcluster-yaml-mcp-server 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vcluster-yaml-mcp-server",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "MCP server for querying vcluster YAML configurations using jq",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/github.js CHANGED
@@ -10,6 +10,20 @@ const REPO_NAME = 'vcluster';
10
10
  const cache = new Map();
11
11
  const CACHE_TTL = 15 * 60 * 1000; // 15 minutes
12
12
 
13
+ // Fetch timeout configuration
14
+ const FETCH_TIMEOUT_MS = 30000; // 30 seconds - generous for large files
15
+
16
+ // Helper to create fetch with timeout
17
+ function createFetchWithTimeout(timeoutMs = FETCH_TIMEOUT_MS) {
18
+ const controller = new AbortController();
19
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
20
+
21
+ return {
22
+ signal: controller.signal,
23
+ cleanup: () => clearTimeout(timeoutId)
24
+ };
25
+ }
26
+
13
27
  class GitHubClient {
14
28
  constructor() {
15
29
  this.defaultBranch = 'main';
@@ -21,12 +35,15 @@ class GitHubClient {
21
35
  const cached = this.getFromCache(cacheKey);
22
36
  if (cached) return cached;
23
37
 
38
+ const { signal, cleanup } = createFetchWithTimeout();
39
+
24
40
  try {
25
41
  const response = await fetch(`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/tags`, {
26
42
  headers: {
27
43
  'Accept': 'application/vnd.github.v3+json',
28
44
  'User-Agent': 'vcluster-yaml-mcp-server'
29
- }
45
+ },
46
+ signal
30
47
  });
31
48
 
32
49
  if (!response.ok) {
@@ -35,12 +52,18 @@ class GitHubClient {
35
52
 
36
53
  const tags = await response.json();
37
54
  const tagNames = tags.map(tag => tag.name);
38
-
55
+
39
56
  this.setCache(cacheKey, tagNames);
40
57
  return tagNames;
41
58
  } catch (error) {
59
+ if (error.name === 'AbortError') {
60
+ console.error('Request timeout: fetching tags took longer than 30s');
61
+ return [];
62
+ }
42
63
  console.error('Error fetching tags:', error);
43
64
  return [];
65
+ } finally {
66
+ cleanup();
44
67
  }
45
68
  }
46
69
 
@@ -50,12 +73,15 @@ class GitHubClient {
50
73
  const cached = this.getFromCache(cacheKey);
51
74
  if (cached) return cached;
52
75
 
76
+ const { signal, cleanup } = createFetchWithTimeout();
77
+
53
78
  try {
54
79
  const response = await fetch(`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/branches`, {
55
80
  headers: {
56
81
  'Accept': 'application/vnd.github.v3+json',
57
82
  'User-Agent': 'vcluster-yaml-mcp-server'
58
- }
83
+ },
84
+ signal
59
85
  });
60
86
 
61
87
  if (!response.ok) {
@@ -64,12 +90,18 @@ class GitHubClient {
64
90
 
65
91
  const branches = await response.json();
66
92
  const branchNames = branches.map(branch => branch.name);
67
-
93
+
68
94
  this.setCache(cacheKey, branchNames);
69
95
  return branchNames;
70
96
  } catch (error) {
97
+ if (error.name === 'AbortError') {
98
+ console.error('Request timeout: fetching branches took longer than 30s');
99
+ return ['main'];
100
+ }
71
101
  console.error('Error fetching branches:', error);
72
102
  return ['main'];
103
+ } finally {
104
+ cleanup();
73
105
  }
74
106
  }
75
107
 
@@ -90,12 +122,15 @@ class GitHubClient {
90
122
  const cached = this.getFromCache(cacheKey);
91
123
  if (cached) return cached;
92
124
 
125
+ const { signal, cleanup } = createFetchWithTimeout();
126
+
93
127
  try {
94
128
  const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${actualRef}/${path}`;
95
129
  const response = await fetch(url, {
96
130
  headers: {
97
131
  'User-Agent': 'vcluster-yaml-mcp-server'
98
- }
132
+ },
133
+ signal
99
134
  });
100
135
 
101
136
  if (!response.ok) {
@@ -109,7 +144,12 @@ class GitHubClient {
109
144
  this.setCache(cacheKey, content);
110
145
  return content;
111
146
  } catch (error) {
147
+ if (error.name === 'AbortError') {
148
+ throw new Error(`Timeout: fetching ${path} took longer than 30s`);
149
+ }
112
150
  throw new Error(`Failed to fetch ${path}: ${error.message}`);
151
+ } finally {
152
+ cleanup();
113
153
  }
114
154
  }
115
155
 
@@ -6,6 +6,7 @@ import express from 'express';
6
6
  import rateLimit from 'express-rate-limit';
7
7
  import { requireApiKey } from './middleware/auth.js';
8
8
  import promClient from 'prom-client';
9
+ import { getHealthInfo, getServerInfo } from './server-info.js';
9
10
 
10
11
  const PORT = process.env.PORT || 3000;
11
12
  const REQUIRE_AUTH = process.env.REQUIRE_AUTH === 'true';
@@ -68,12 +69,7 @@ const mcpLimiter = rateLimit({
68
69
 
69
70
  // Health check endpoint
70
71
  app.get('/health', apiLimiter, (_req, res) => {
71
- res.json({
72
- status: 'ok',
73
- name: 'vcluster-yaml-mcp-server',
74
- version: '0.1.0',
75
- timestamp: new Date().toISOString()
76
- });
72
+ res.json(getHealthInfo());
77
73
  });
78
74
 
79
75
  // Prometheus metrics endpoint
@@ -85,15 +81,12 @@ app.get('/metrics', async (_req, res) => {
85
81
  // Root endpoint info
86
82
  app.get('/', (_req, res) => {
87
83
  res.json({
88
- name: 'vcluster-yaml-mcp-server',
89
- version: '0.1.0',
90
- description: 'MCP server for querying vCluster YAML configurations',
84
+ ...getServerInfo(),
91
85
  endpoints: {
92
86
  mcp: '/mcp',
93
87
  health: '/health',
94
88
  metrics: '/metrics'
95
- },
96
- documentation: 'https://github.com/Piotr1215/vcluster-yaml-mcp-server'
89
+ }
97
90
  });
98
91
  });
99
92
 
@@ -0,0 +1,72 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ // Load package.json once at module level
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const packageJson = JSON.parse(
9
+ await readFile(join(__dirname, '../package.json'), 'utf-8')
10
+ );
11
+
12
+ // Build metadata from environment (injected by Docker/CI)
13
+ const buildInfo = {
14
+ version: packageJson.version,
15
+ gitSha: process.env.GIT_SHA || 'unknown',
16
+ buildDate: process.env.BUILD_DATE || 'unknown',
17
+ imageVersion: process.env.IMAGE_VERSION || packageJson.version
18
+ };
19
+
20
+ /**
21
+ * Get complete server information including version, build info, and runtime details
22
+ * @returns {Object} Server metadata object
23
+ */
24
+ export function getServerInfo() {
25
+ return {
26
+ name: 'vcluster-yaml-mcp-server',
27
+ description: packageJson.description,
28
+ version: buildInfo.version,
29
+ repository: 'https://github.com/Piotr1215/vcluster-yaml-mcp-server',
30
+ documentation: 'https://github.com/Piotr1215/vcluster-yaml-mcp-server#readme',
31
+ license: 'MIT',
32
+ build: {
33
+ gitSha: buildInfo.gitSha,
34
+ buildDate: buildInfo.buildDate,
35
+ imageVersion: buildInfo.imageVersion
36
+ },
37
+ runtime: {
38
+ nodeVersion: process.version,
39
+ platform: process.platform,
40
+ arch: process.arch
41
+ }
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Get simplified version info for health checks
47
+ * @returns {Object} Health check metadata
48
+ */
49
+ export function getHealthInfo() {
50
+ return {
51
+ status: 'ok',
52
+ name: 'vcluster-yaml-mcp-server',
53
+ version: packageJson.version,
54
+ image: {
55
+ version: buildInfo.imageVersion,
56
+ gitSha: buildInfo.gitSha,
57
+ buildDate: buildInfo.buildDate
58
+ },
59
+ timestamp: new Date().toISOString()
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Get basic server metadata for MCP Server constructor
65
+ * @returns {Object} MCP server metadata
66
+ */
67
+ export function getMcpServerInfo() {
68
+ return {
69
+ name: 'vcluster-yaml-mcp-server',
70
+ version: packageJson.version
71
+ };
72
+ }
package/src/server.js CHANGED
@@ -1,17 +1,21 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ import {
3
+ ListToolsRequestSchema,
4
+ CallToolRequestSchema,
5
+ ListResourcesRequestSchema,
6
+ ReadResourceRequestSchema
7
+ } from '@modelcontextprotocol/sdk/types.js';
3
8
  import { githubClient } from './github.js';
4
9
  import { executeToolHandler } from './tool-registry.js';
10
+ import { getMcpServerInfo, getServerInfo } from './server-info.js';
5
11
 
6
12
  export function createServer() {
7
13
  const server = new Server(
8
- {
9
- name: 'vcluster-yaml-mcp-server',
10
- version: '0.1.0'
11
- },
14
+ getMcpServerInfo(),
12
15
  {
13
16
  capabilities: {
14
- tools: {}
17
+ tools: {},
18
+ resources: {}
15
19
  }
16
20
  }
17
21
  );
@@ -128,5 +132,37 @@ export function createServer() {
128
132
  return executeToolHandler(name, args, githubClient);
129
133
  });
130
134
 
135
+ // Resource handlers
136
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
137
+ return {
138
+ resources: [
139
+ {
140
+ uri: 'server://info',
141
+ name: 'Server Information',
142
+ description: 'Version, build info, and metadata about this MCP server',
143
+ mimeType: 'application/json'
144
+ }
145
+ ]
146
+ };
147
+ });
148
+
149
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
150
+ const { uri } = request.params;
151
+
152
+ if (uri === 'server://info') {
153
+ return {
154
+ contents: [
155
+ {
156
+ uri: 'server://info',
157
+ mimeType: 'application/json',
158
+ text: JSON.stringify(getServerInfo(), null, 2)
159
+ }
160
+ ]
161
+ };
162
+ }
163
+
164
+ throw new Error(`Unknown resource: ${uri}`);
165
+ });
166
+
131
167
  return server;
132
168
  }
@@ -186,38 +186,48 @@ export async function handleListVersions(args, githubClient) {
186
186
  /**
187
187
  * Handle: smart-query
188
188
  * Pure function except for I/O
189
+ * CRITICAL: Must never fail - always return helpful results or fallback
189
190
  */
190
191
  export async function handleSmartQuery(args, githubClient) {
191
192
  const { query, version = 'main', file = 'chart/values.yaml' } = args;
192
193
 
193
- const yamlData = await githubClient.getYamlContent(file, version);
194
- const searchTerm = query.toLowerCase();
194
+ try {
195
+ const yamlData = await githubClient.getYamlContent(file, version);
196
+ const searchTerm = query.toLowerCase();
195
197
 
196
- // Extract all paths (pure function)
197
- const allInfo = extractYamlInfo(yamlData);
198
+ // Extract all paths (pure function)
199
+ const allInfo = extractYamlInfo(yamlData);
198
200
 
199
- // Search (pure function)
200
- const results = searchYaml(allInfo, searchTerm);
201
+ // Search (pure function)
202
+ const results = searchYaml(allInfo, searchTerm);
201
203
 
202
- // Handle no matches
203
- if (results.length === 0) {
204
- const similarPaths = findSimilarPaths(allInfo, searchTerm);
205
- const formatted = formatNoMatches({ query, fileName: file, version, similarPaths, yamlData });
206
- return buildSuccessResponse(formatted);
207
- }
204
+ // Handle no matches
205
+ if (results.length === 0) {
206
+ const similarPaths = findSimilarPaths(allInfo, searchTerm);
207
+ const formatted = formatNoMatches({ query, fileName: file, version, similarPaths, yamlData });
208
+ return buildSuccessResponse(formatted);
209
+ }
208
210
 
209
- // Sort by relevance (pure function)
210
- const sorted = sortByRelevance(results, searchTerm);
211
+ // Sort by relevance (pure function)
212
+ const sorted = sortByRelevance(results, searchTerm);
211
213
 
212
- // Format results
213
- const formatted = formatQueryResults(sorted, {
214
- query,
215
- fileName: file,
216
- version,
217
- maxResults: 50
218
- });
214
+ // Format results
215
+ const formatted = formatQueryResults(sorted, {
216
+ query,
217
+ fileName: file,
218
+ version,
219
+ maxResults: 50
220
+ });
219
221
 
220
- return buildSuccessResponse(formatted);
222
+ return buildSuccessResponse(formatted);
223
+ } catch (error) {
224
+ // Graceful fallback - always provide helpful message
225
+ const errorMsg = error.message.includes('Timeout')
226
+ ? `⏱️ Request timed out while fetching ${file} (version: ${version}).\n\n**Suggestions:**\n- Try a different version (e.g., "v0.29.1")\n- The file might be temporarily unavailable\n- Check if the file path is correct`
227
+ : `❌ Error searching for "${query}" in ${file} (version: ${version}):\n${error.message}\n\n**Suggestions:**\n- Try "list-versions" to see available versions\n- Verify the file path is correct`;
228
+
229
+ return buildSuccessResponse(errorMsg);
230
+ }
221
231
  }
222
232
 
223
233
  /**
@@ -227,10 +237,24 @@ export async function handleSmartQuery(args, githubClient) {
227
237
  export async function handleExtractRules(args, githubClient) {
228
238
  const { version = 'main', file = 'chart/values.yaml', section } = args;
229
239
 
230
- const content = await githubClient.getFileContent(file, version);
231
- const rules = extractValidationRulesFromComments(content, section);
232
-
233
- return buildSuccessResponse(JSON.stringify(rules, null, 2));
240
+ try {
241
+ const content = await githubClient.getFileContent(file, version);
242
+ const rules = extractValidationRulesFromComments(content, section);
243
+
244
+ // Remove originalComments to reduce response size
245
+ const optimizedRules = {
246
+ ...rules,
247
+ rules: rules.rules.map(r => ({
248
+ path: r.path,
249
+ instructions: r.instructions
250
+ }))
251
+ };
252
+
253
+ // No pretty-printing to reduce size
254
+ return buildSuccessResponse(JSON.stringify(optimizedRules));
255
+ } catch (error) {
256
+ return buildErrorResponse(`Failed to extract validation rules: ${error.message}`);
257
+ }
234
258
  }
235
259
 
236
260
  /**