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 +1 -1
- package/src/github.js +45 -5
- package/src/http-server.js +4 -11
- package/src/server-info.js +72 -0
- package/src/server.js +42 -6
- package/src/tool-handlers.js +50 -26
package/package.json
CHANGED
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
|
|
package/src/http-server.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
}
|
package/src/tool-handlers.js
CHANGED
|
@@ -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
|
-
|
|
194
|
-
|
|
194
|
+
try {
|
|
195
|
+
const yamlData = await githubClient.getYamlContent(file, version);
|
|
196
|
+
const searchTerm = query.toLowerCase();
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
// Extract all paths (pure function)
|
|
199
|
+
const allInfo = extractYamlInfo(yamlData);
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
// Search (pure function)
|
|
202
|
+
const results = searchYaml(allInfo, searchTerm);
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
+
// Sort by relevance (pure function)
|
|
212
|
+
const sorted = sortByRelevance(results, searchTerm);
|
|
211
213
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
214
|
+
// Format results
|
|
215
|
+
const formatted = formatQueryResults(sorted, {
|
|
216
|
+
query,
|
|
217
|
+
fileName: file,
|
|
218
|
+
version,
|
|
219
|
+
maxResults: 50
|
|
220
|
+
});
|
|
219
221
|
|
|
220
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
/**
|