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 +292 -0
- package/package.json +52 -0
- package/src/cli-handlers.js +290 -0
- package/src/cli.js +128 -0
- package/src/formatters.js +168 -0
- package/src/github.js +170 -0
- package/src/http-server.js +67 -0
- package/src/index.js +11 -0
- package/src/middleware/auth.js +25 -0
- package/src/schema-validator.js +351 -0
- package/src/server.js +1006 -0
- package/src/snippet-validator.js +370 -0
- package/src/snippet-validator.test.js +366 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* vCluster CLI - Standalone command-line interface
|
|
5
|
+
* Provides query, validate, and list-versions commands
|
|
6
|
+
* Wraps MCP server functionality with user-friendly CLI
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { handleQuery, handleListVersions, handleValidate } from './cli-handlers.js';
|
|
11
|
+
import { formatOutput } from './formatters.js';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('vcluster-yaml')
|
|
17
|
+
.description('vCluster YAML configuration CLI')
|
|
18
|
+
.version('0.1.0');
|
|
19
|
+
|
|
20
|
+
// Query command
|
|
21
|
+
program
|
|
22
|
+
.command('query <query>')
|
|
23
|
+
.description('Search for vCluster configuration fields')
|
|
24
|
+
.option('--file <file>', 'Configuration file to search', 'chart/values.yaml')
|
|
25
|
+
.option('--version <version>', 'vCluster version or branch', 'main')
|
|
26
|
+
.option('-f, --format <format>', 'Output format (json, yaml, table)', 'json')
|
|
27
|
+
.action(async (query, options) => {
|
|
28
|
+
try {
|
|
29
|
+
// Validate format option
|
|
30
|
+
if (!['json', 'yaml', 'table'].includes(options.format)) {
|
|
31
|
+
console.error(`Error: Invalid format "${options.format}". Must be one of: json, yaml, table`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await handleQuery(query, {
|
|
36
|
+
file: options.file,
|
|
37
|
+
version: options.version
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
// Error case - output error message and exit with code 1
|
|
42
|
+
if (options.format === 'json') {
|
|
43
|
+
console.log(JSON.stringify(result, null, 2));
|
|
44
|
+
} else {
|
|
45
|
+
console.error(result.error);
|
|
46
|
+
}
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const output = formatOutput(result, options.format, 'query');
|
|
51
|
+
console.log(output);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Error: ${error.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// List versions command
|
|
60
|
+
program
|
|
61
|
+
.command('list-versions')
|
|
62
|
+
.description('List available vCluster versions')
|
|
63
|
+
.option('-f, --format <format>', 'Output format (json, yaml, table)', 'json')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
try {
|
|
66
|
+
// Validate format option
|
|
67
|
+
if (!['json', 'yaml', 'table'].includes(options.format)) {
|
|
68
|
+
console.error(`Error: Invalid format "${options.format}". Must be one of: json, yaml, table`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = await handleListVersions();
|
|
73
|
+
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
if (options.format === 'json') {
|
|
76
|
+
console.log(JSON.stringify(result, null, 2));
|
|
77
|
+
} else {
|
|
78
|
+
console.error(result.error);
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const output = formatOutput(result, options.format, 'list-versions');
|
|
84
|
+
console.log(output);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Error: ${error.message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Validate command
|
|
93
|
+
program
|
|
94
|
+
.command('validate <content>')
|
|
95
|
+
.description('Validate vCluster configuration')
|
|
96
|
+
.option('--version <version>', 'vCluster version for schema', 'main')
|
|
97
|
+
.option('-f, --format <format>', 'Output format (json, yaml, table)', 'json')
|
|
98
|
+
.action(async (content, options) => {
|
|
99
|
+
try {
|
|
100
|
+
// Validate format option
|
|
101
|
+
if (!['json', 'yaml', 'table'].includes(options.format)) {
|
|
102
|
+
console.error(`Error: Invalid format "${options.format}". Must be one of: json, yaml, table`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await handleValidate(content, {
|
|
107
|
+
version: options.version
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// For validation, always output the result (even if not successful)
|
|
111
|
+
// The formatOutput will handle error cases appropriately
|
|
112
|
+
const output = formatOutput(result, options.format, 'validate');
|
|
113
|
+
console.log(output);
|
|
114
|
+
|
|
115
|
+
// Exit with code 1 if validation failed OR if we couldn't load schema
|
|
116
|
+
if (!result.success || !result.valid) {
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.exit(0);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`Error: ${error.message}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Parse arguments
|
|
128
|
+
program.parse();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatters for CLI
|
|
3
|
+
* Provides JSON, YAML, and table formatting for command outputs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import Table from 'cli-table3';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format output as JSON
|
|
12
|
+
* Always returns valid JSON that can be parsed with JSON.parse()
|
|
13
|
+
*/
|
|
14
|
+
export function formatJSON(data) {
|
|
15
|
+
return JSON.stringify(data, null, 2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format output as YAML
|
|
20
|
+
* Always returns valid YAML that can be parsed with yaml.load()
|
|
21
|
+
*/
|
|
22
|
+
export function formatYAML(data) {
|
|
23
|
+
return yaml.dump(data, {
|
|
24
|
+
indent: 2,
|
|
25
|
+
lineWidth: 120,
|
|
26
|
+
noRefs: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format query results as a table
|
|
32
|
+
* Returns formatted table with box-drawing characters and colored headers
|
|
33
|
+
*/
|
|
34
|
+
export function formatQueryTable(results, metadata) {
|
|
35
|
+
// Handle empty results
|
|
36
|
+
if (!results || results.length === 0) {
|
|
37
|
+
return `No results found for query: "${metadata.query}"`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const table = new Table({
|
|
41
|
+
head: [
|
|
42
|
+
chalk.cyan('Field'),
|
|
43
|
+
chalk.cyan('Value'),
|
|
44
|
+
chalk.cyan('Type'),
|
|
45
|
+
chalk.cyan('Description')
|
|
46
|
+
],
|
|
47
|
+
style: {
|
|
48
|
+
head: [],
|
|
49
|
+
border: []
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
results.forEach(result => {
|
|
54
|
+
table.push([
|
|
55
|
+
result.field || result.path || '',
|
|
56
|
+
formatValue(result.value),
|
|
57
|
+
result.type || '',
|
|
58
|
+
result.description || ''
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return table.toString();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format list-versions results as a table
|
|
67
|
+
*/
|
|
68
|
+
export function formatVersionsTable(versions) {
|
|
69
|
+
if (!versions || versions.length === 0) {
|
|
70
|
+
return 'No versions found';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const table = new Table({
|
|
74
|
+
head: [chalk.cyan('Version')],
|
|
75
|
+
style: {
|
|
76
|
+
head: [],
|
|
77
|
+
border: []
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
versions.forEach(version => {
|
|
82
|
+
table.push([version]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return table.toString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format validation results as a table
|
|
90
|
+
*/
|
|
91
|
+
export function formatValidationTable(data) {
|
|
92
|
+
if (data.valid) {
|
|
93
|
+
return chalk.green('✓ Configuration is valid');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let output = chalk.red('✗ Configuration has errors:\n');
|
|
97
|
+
|
|
98
|
+
const table = new Table({
|
|
99
|
+
head: [
|
|
100
|
+
chalk.cyan('Path'),
|
|
101
|
+
chalk.cyan('Error'),
|
|
102
|
+
chalk.cyan('Type')
|
|
103
|
+
],
|
|
104
|
+
style: {
|
|
105
|
+
head: [],
|
|
106
|
+
border: []
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
data.errors.forEach(error => {
|
|
111
|
+
table.push([
|
|
112
|
+
error.path || 'root',
|
|
113
|
+
error.message || '',
|
|
114
|
+
error.type || ''
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
output += table.toString();
|
|
119
|
+
return output;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Helper function to format values for table display
|
|
124
|
+
* Truncates long values and handles different types
|
|
125
|
+
*/
|
|
126
|
+
function formatValue(value) {
|
|
127
|
+
if (value === null || value === undefined) {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof value === 'object') {
|
|
132
|
+
const str = JSON.stringify(value);
|
|
133
|
+
return str.length > 50 ? str.substring(0, 47) + '...' : str;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const str = String(value);
|
|
137
|
+
return str.length > 50 ? str.substring(0, 47) + '...' : str;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Main formatter function
|
|
142
|
+
* Routes to appropriate formatter based on format option
|
|
143
|
+
*/
|
|
144
|
+
export function formatOutput(data, format, command) {
|
|
145
|
+
switch (format) {
|
|
146
|
+
case 'json':
|
|
147
|
+
return formatJSON(data);
|
|
148
|
+
|
|
149
|
+
case 'yaml':
|
|
150
|
+
return formatYAML(data);
|
|
151
|
+
|
|
152
|
+
case 'table':
|
|
153
|
+
// Route to appropriate table formatter based on command/data shape
|
|
154
|
+
if (command === 'query' || data.results !== undefined) {
|
|
155
|
+
return formatQueryTable(data.results || [], data.metadata || {});
|
|
156
|
+
} else if (command === 'list-versions' || data.versions !== undefined) {
|
|
157
|
+
return formatVersionsTable(data.versions || []);
|
|
158
|
+
} else if (command === 'validate' || data.valid !== undefined) {
|
|
159
|
+
return formatValidationTable(data);
|
|
160
|
+
} else {
|
|
161
|
+
// Fallback: format entire data object as JSON in table
|
|
162
|
+
return formatJSON(data);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
default:
|
|
166
|
+
throw new Error(`Unknown format: ${format}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/github.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
|
|
4
|
+
const GITHUB_API_BASE = 'https://api.github.com';
|
|
5
|
+
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com';
|
|
6
|
+
const REPO_OWNER = 'loft-sh';
|
|
7
|
+
const REPO_NAME = 'vcluster';
|
|
8
|
+
|
|
9
|
+
// Cache for fetched content (15 minute TTL)
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
const CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
12
|
+
|
|
13
|
+
class GitHubClient {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.defaultBranch = 'main';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Get list of available tags (versions)
|
|
19
|
+
async getTags() {
|
|
20
|
+
const cacheKey = 'tags';
|
|
21
|
+
const cached = this.getFromCache(cacheKey);
|
|
22
|
+
if (cached) return cached;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/tags`, {
|
|
26
|
+
headers: {
|
|
27
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
28
|
+
'User-Agent': 'vcluster-yaml-mcp-server'
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(`GitHub API error: ${response.statusText}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tags = await response.json();
|
|
37
|
+
const tagNames = tags.map(tag => tag.name);
|
|
38
|
+
|
|
39
|
+
this.setCache(cacheKey, tagNames);
|
|
40
|
+
return tagNames;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error fetching tags:', error);
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get list of branches
|
|
48
|
+
async getBranches() {
|
|
49
|
+
const cacheKey = 'branches';
|
|
50
|
+
const cached = this.getFromCache(cacheKey);
|
|
51
|
+
if (cached) return cached;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/branches`, {
|
|
55
|
+
headers: {
|
|
56
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
57
|
+
'User-Agent': 'vcluster-yaml-mcp-server'
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`GitHub API error: ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const branches = await response.json();
|
|
66
|
+
const branchNames = branches.map(branch => branch.name);
|
|
67
|
+
|
|
68
|
+
this.setCache(cacheKey, branchNames);
|
|
69
|
+
return branchNames;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error fetching branches:', error);
|
|
72
|
+
return ['main'];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get file content from GitHub
|
|
77
|
+
async getFileContent(path, ref = 'main') {
|
|
78
|
+
const actualRef = ref;
|
|
79
|
+
const cacheKey = `file:${actualRef}:${path}`;
|
|
80
|
+
const cached = this.getFromCache(cacheKey);
|
|
81
|
+
if (cached) return cached;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${actualRef}/${path}`;
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
headers: {
|
|
87
|
+
'User-Agent': 'vcluster-yaml-mcp-server'
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
if (response.status === 404) {
|
|
93
|
+
throw new Error(`File not found: ${path} (ref: ${actualRef})`);
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`GitHub error: ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const content = await response.text();
|
|
99
|
+
this.setCache(cacheKey, content);
|
|
100
|
+
return content;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw new Error(`Failed to fetch ${path}: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get parsed YAML content
|
|
107
|
+
async getYamlContent(path, ref = null) {
|
|
108
|
+
const content = await this.getFileContent(path, ref);
|
|
109
|
+
try {
|
|
110
|
+
return yaml.load(content);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`Failed to parse YAML from ${path}: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get vcluster configuration files
|
|
117
|
+
async getVClusterConfigs(ref = null) {
|
|
118
|
+
const configs = {};
|
|
119
|
+
|
|
120
|
+
// Known vcluster config files
|
|
121
|
+
const configPaths = [
|
|
122
|
+
'chart/values.yaml',
|
|
123
|
+
'chart/values.schema.json',
|
|
124
|
+
'config/values.yaml',
|
|
125
|
+
'values.schema.json'
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (const path of configPaths) {
|
|
129
|
+
try {
|
|
130
|
+
if (path.endsWith('.yaml') || path.endsWith('.yml')) {
|
|
131
|
+
configs[path] = await this.getYamlContent(path, ref);
|
|
132
|
+
} else if (path.endsWith('.json')) {
|
|
133
|
+
const content = await this.getFileContent(path, ref);
|
|
134
|
+
configs[path] = JSON.parse(content);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// File might not exist in this version, skip it
|
|
138
|
+
console.debug(`Skipping ${path}: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return configs;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Cache helpers
|
|
146
|
+
getFromCache(key) {
|
|
147
|
+
const item = cache.get(key);
|
|
148
|
+
if (!item) return null;
|
|
149
|
+
|
|
150
|
+
if (Date.now() - item.timestamp > CACHE_TTL) {
|
|
151
|
+
cache.delete(key);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return item.data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setCache(key, data) {
|
|
159
|
+
cache.set(key, {
|
|
160
|
+
data,
|
|
161
|
+
timestamp: Date.now()
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
clearCache() {
|
|
166
|
+
cache.clear();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const githubClient = new GitHubClient();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { createServer } from './server.js';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { requireApiKey } from './middleware/auth.js';
|
|
7
|
+
|
|
8
|
+
const PORT = process.env.PORT || 3000;
|
|
9
|
+
const REQUIRE_AUTH = process.env.REQUIRE_AUTH === 'true';
|
|
10
|
+
|
|
11
|
+
const app = express();
|
|
12
|
+
app.use(express.json());
|
|
13
|
+
|
|
14
|
+
// Health check endpoint
|
|
15
|
+
app.get('/health', (_req, res) => {
|
|
16
|
+
res.json({
|
|
17
|
+
status: 'ok',
|
|
18
|
+
name: 'vcluster-yaml-mcp-server',
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
timestamp: new Date().toISOString()
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Root endpoint info
|
|
25
|
+
app.get('/', (_req, res) => {
|
|
26
|
+
res.json({
|
|
27
|
+
name: 'vcluster-yaml-mcp-server',
|
|
28
|
+
version: '0.1.0',
|
|
29
|
+
description: 'MCP server for querying vCluster YAML configurations',
|
|
30
|
+
endpoints: {
|
|
31
|
+
mcp: '/mcp',
|
|
32
|
+
health: '/health'
|
|
33
|
+
},
|
|
34
|
+
documentation: 'https://github.com/Piotr1215/vcluster-yaml-mcp-server'
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// MCP endpoint with Streamable HTTP transport
|
|
39
|
+
const mcpHandler = async (req, res) => {
|
|
40
|
+
console.log(`MCP ${req.method} request received`);
|
|
41
|
+
|
|
42
|
+
// Create new transport per request to prevent ID collisions
|
|
43
|
+
const transport = new StreamableHTTPServerTransport({
|
|
44
|
+
sessionIdGenerator: undefined,
|
|
45
|
+
enableJsonResponse: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Cleanup on connection close
|
|
49
|
+
res.on('close', () => {
|
|
50
|
+
transport.close();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const server = createServer();
|
|
54
|
+
await server.connect(transport);
|
|
55
|
+
await transport.handleRequest(req, res, req.body);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 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);
|
|
61
|
+
|
|
62
|
+
app.listen(PORT, () => {
|
|
63
|
+
console.log(`vcluster-yaml-mcp-server HTTP running on port ${PORT}`);
|
|
64
|
+
console.log(`Health check: http://localhost:${PORT}/health`);
|
|
65
|
+
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
|
|
66
|
+
console.log(`Transport: Streamable HTTP (MCP 2025-03-26)`);
|
|
67
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { createServer } from './server.js';
|
|
5
|
+
|
|
6
|
+
// Create server (no config path needed - uses GitHub)
|
|
7
|
+
const server = createServer();
|
|
8
|
+
|
|
9
|
+
// Start the server
|
|
10
|
+
const transport = new StdioServerTransport();
|
|
11
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Simple API key authentication middleware
|
|
2
|
+
export function requireApiKey(req, res, next) {
|
|
3
|
+
const authHeader = req.headers['authorization'];
|
|
4
|
+
|
|
5
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
6
|
+
return res.status(401).json({
|
|
7
|
+
error: 'Unauthorized',
|
|
8
|
+
message: 'Missing or invalid Authorization header'
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const token = authHeader.substring(7);
|
|
13
|
+
const validTokens = (process.env.VALID_API_KEYS || '').split(',');
|
|
14
|
+
|
|
15
|
+
if (!validTokens.includes(token)) {
|
|
16
|
+
return res.status(403).json({
|
|
17
|
+
error: 'Forbidden',
|
|
18
|
+
message: 'Invalid API key'
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Optional: Track usage per token
|
|
23
|
+
req.apiKey = token;
|
|
24
|
+
next();
|
|
25
|
+
}
|