@mithung/vunet-mcp-server 2.0.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/CHANGELOG.md +79 -0
- package/DATA_MODELS.md +192 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +213 -0
- package/README.md +679 -0
- package/SETUP.md +325 -0
- package/config.example.json +90 -0
- package/index.js +558 -0
- package/mcp-config.example.json +42 -0
- package/package.json +62 -0
package/index.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import axios from "axios";
|
|
10
|
+
import https from "https";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Vunet MCP Server
|
|
14
|
+
* Connects to Vunet vuSmartMaps using environment variables (similar to Dynatrace MCP)
|
|
15
|
+
*
|
|
16
|
+
* Environment Variables:
|
|
17
|
+
* - VUNET_TENANT_URL: Vunet tenant URL (required)
|
|
18
|
+
* - VUNET_USERNAME: Username for authentication (required)
|
|
19
|
+
* - VUNET_PASSWORD: Password for authentication (required)
|
|
20
|
+
* - VUNET_BU_ID: Business Unit ID (optional, default: "1")
|
|
21
|
+
* - VUNET_VERIFY_SSL: SSL verification (optional, default: "true")
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
class VunetMCPServer {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.server = new Server(
|
|
27
|
+
{
|
|
28
|
+
name: "vunet-mcp-server",
|
|
29
|
+
version: "2.0.0",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
capabilities: {
|
|
33
|
+
tools: {},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Configuration from environment variables
|
|
39
|
+
this.tenantUrl = process.env.VUNET_TENANT_URL;
|
|
40
|
+
this.username = process.env.VUNET_USERNAME;
|
|
41
|
+
this.password = process.env.VUNET_PASSWORD;
|
|
42
|
+
this.buId = process.env.VUNET_BU_ID || "1";
|
|
43
|
+
this.bearerToken = process.env.VUNET_BEARER_TOKEN;
|
|
44
|
+
|
|
45
|
+
// Session state
|
|
46
|
+
this.sessionToken = null;
|
|
47
|
+
this.sessionExpiry = null;
|
|
48
|
+
this.isAuthenticated = false;
|
|
49
|
+
|
|
50
|
+
// HTTPS agent for SSL handling
|
|
51
|
+
const verifySSL = process.env.VUNET_VERIFY_SSL !== "false";
|
|
52
|
+
this.httpsAgent = new https.Agent({
|
|
53
|
+
rejectUnauthorized: verifySSL,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.setupHandlers();
|
|
57
|
+
this.setupErrorHandling();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setupErrorHandling() {
|
|
61
|
+
this.server.onerror = (error) => {
|
|
62
|
+
console.error("[MCP Error]", error);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
process.on("SIGINT", async () => {
|
|
66
|
+
await this.cleanup();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async cleanup() {
|
|
72
|
+
// Logout from tenant
|
|
73
|
+
if (this.isAuthenticated && this.sessionToken) {
|
|
74
|
+
try {
|
|
75
|
+
await this.logout();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`Failed to logout:`, error.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Normalize tenant URL by removing trailing slashes
|
|
84
|
+
*/
|
|
85
|
+
normalizeTenantUrl(url) {
|
|
86
|
+
return url.replace(/\/+$/, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Ensure we have a valid session, authenticate if needed
|
|
91
|
+
*/
|
|
92
|
+
async ensureAuthenticated() {
|
|
93
|
+
// If bearer token is provided, use it directly
|
|
94
|
+
if (this.bearerToken) {
|
|
95
|
+
this.sessionToken = this.bearerToken;
|
|
96
|
+
this.isAuthenticated = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if credentials are configured
|
|
101
|
+
if (!this.tenantUrl || !this.username || !this.password) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Vunet tenant not configured. Please set VUNET_TENANT_URL, VUNET_USERNAME, and VUNET_PASSWORD environment variables."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if we have a valid session
|
|
108
|
+
if (this.isAuthenticated && this.sessionToken) {
|
|
109
|
+
if (this.sessionExpiry && Date.now() < this.sessionExpiry) {
|
|
110
|
+
return; // Session is still valid
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Need to authenticate
|
|
115
|
+
await this.login();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Login to Vunet tenant
|
|
120
|
+
*/
|
|
121
|
+
async login() {
|
|
122
|
+
const url = `${this.normalizeTenantUrl(this.tenantUrl)}/vuSmartMaps/api/1/bu/${this.buId}/auth/users/login/`;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const response = await axios.post(
|
|
126
|
+
url,
|
|
127
|
+
{
|
|
128
|
+
username: this.username,
|
|
129
|
+
password: this.password,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
httpsAgent: this.httpsAgent,
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (response.data && response.data.access_token) {
|
|
137
|
+
this.sessionToken = response.data.access_token;
|
|
138
|
+
|
|
139
|
+
// Set expiry to 1 hour from now (adjust based on actual token expiry if available)
|
|
140
|
+
this.sessionExpiry = Date.now() + 60 * 60 * 1000;
|
|
141
|
+
this.isAuthenticated = true;
|
|
142
|
+
|
|
143
|
+
console.error(`[Vunet MCP] Successfully authenticated to ${this.tenantUrl}`);
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error("No access token in response");
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.isAuthenticated = false;
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Failed to authenticate to Vunet: ${error.response?.data?.message || error.message}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Logout from Vunet tenant
|
|
157
|
+
*/
|
|
158
|
+
async logout() {
|
|
159
|
+
if (!this.sessionToken) return;
|
|
160
|
+
|
|
161
|
+
const url = `${this.normalizeTenantUrl(this.tenantUrl)}/vuSmartMaps/api/1/bu/${this.buId}/auth/users/logout/`;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await axios.post(
|
|
165
|
+
url,
|
|
166
|
+
{},
|
|
167
|
+
{
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
170
|
+
},
|
|
171
|
+
httpsAgent: this.httpsAgent,
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
console.error(`[Vunet MCP] Successfully logged out from ${this.tenantUrl}`);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(`[Vunet MCP] Logout failed: ${error.message}`);
|
|
178
|
+
} finally {
|
|
179
|
+
this.sessionToken = null;
|
|
180
|
+
this.sessionExpiry = null;
|
|
181
|
+
this.isAuthenticated = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build query parameters for metric query
|
|
187
|
+
*/
|
|
188
|
+
buildQueryParams(args) {
|
|
189
|
+
const params = {};
|
|
190
|
+
|
|
191
|
+
// Time parameters
|
|
192
|
+
if (args.relative_time) {
|
|
193
|
+
params.relative_time = args.relative_time;
|
|
194
|
+
} else if (args.start_time && args.end_time) {
|
|
195
|
+
params.start_time = args.start_time;
|
|
196
|
+
params.end_time = args.end_time;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Filters (dynamic)
|
|
200
|
+
if (args.filters && typeof args.filters === "object") {
|
|
201
|
+
Object.assign(params, args.filters);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Field inclusion/exclusion
|
|
205
|
+
if (args.include) params.include = args.include;
|
|
206
|
+
if (args.exclude) params.exclude = args.exclude;
|
|
207
|
+
|
|
208
|
+
// Other options
|
|
209
|
+
if (args.thresholds !== undefined) params.thresholds = args.thresholds;
|
|
210
|
+
if (args.formatting) params.formatting = args.formatting;
|
|
211
|
+
if (args.add_on_time_intervals) params.add_on_time_intervals = args.add_on_time_intervals;
|
|
212
|
+
if (args.add_on_data_models) params.add_on_data_models = args.add_on_data_models;
|
|
213
|
+
if (args.time_shift) params.time_shift = args.time_shift;
|
|
214
|
+
|
|
215
|
+
return params;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Query a metric/data model
|
|
220
|
+
*/
|
|
221
|
+
async queryMetric(metricName, queryParams) {
|
|
222
|
+
await this.ensureAuthenticated();
|
|
223
|
+
|
|
224
|
+
const url = `${this.normalizeTenantUrl(this.tenantUrl)}/api/metrics/${metricName}/`;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const response = await axios.get(url, {
|
|
228
|
+
params: queryParams,
|
|
229
|
+
headers: {
|
|
230
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
231
|
+
},
|
|
232
|
+
httpsAgent: this.httpsAgent,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return response.data;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error.response?.status === 401) {
|
|
238
|
+
// Session expired, try to re-authenticate
|
|
239
|
+
this.isAuthenticated = false;
|
|
240
|
+
await this.ensureAuthenticated();
|
|
241
|
+
|
|
242
|
+
// Retry the request
|
|
243
|
+
const retryResponse = await axios.get(url, {
|
|
244
|
+
params: queryParams,
|
|
245
|
+
headers: {
|
|
246
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
247
|
+
},
|
|
248
|
+
httpsAgent: this.httpsAgent,
|
|
249
|
+
});
|
|
250
|
+
return retryResponse.data;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Failed to query metric '${metricName}': ${error.response?.data?.message || error.message}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setupHandlers() {
|
|
260
|
+
// List available tools
|
|
261
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
262
|
+
tools: [
|
|
263
|
+
{
|
|
264
|
+
name: "vunet_query_metric",
|
|
265
|
+
description: "Query a Vunet data model/metric with flexible time ranges and filters. Automatically uses the configured tenant.",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
metric_name: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Name of the data model/metric to query (e.g., 'datamodel-metric-api')",
|
|
272
|
+
},
|
|
273
|
+
relative_time: {
|
|
274
|
+
type: "string",
|
|
275
|
+
description: "Relative time (e.g., '15m', '1h', '1d', '1w', '1M', '1y')",
|
|
276
|
+
},
|
|
277
|
+
start_time: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Start time (epoch timestamp or 'now', 'now-1h', etc.). Use with end_time instead of relative_time.",
|
|
280
|
+
},
|
|
281
|
+
end_time: {
|
|
282
|
+
type: "string",
|
|
283
|
+
description: "End time (epoch timestamp or 'now', 'now-1h', etc.). Use with start_time instead of relative_time.",
|
|
284
|
+
},
|
|
285
|
+
filters: {
|
|
286
|
+
type: "object",
|
|
287
|
+
description: "Dynamic filters as key-value pairs (e.g., {\"ip\": \"192.168.0.1\", \"status\": \"active\"})",
|
|
288
|
+
},
|
|
289
|
+
include: {
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Comma-separated fields to include in response",
|
|
292
|
+
},
|
|
293
|
+
exclude: {
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "Comma-separated fields to exclude from response",
|
|
296
|
+
},
|
|
297
|
+
thresholds: {
|
|
298
|
+
type: "boolean",
|
|
299
|
+
description: "Include threshold data in response (default: false)",
|
|
300
|
+
},
|
|
301
|
+
formatting: {
|
|
302
|
+
type: "string",
|
|
303
|
+
description: "Response format. Use 'lama' for LAMA format (Log Analytics)",
|
|
304
|
+
enum: ["lama"],
|
|
305
|
+
},
|
|
306
|
+
add_on_time_intervals: {
|
|
307
|
+
type: "array",
|
|
308
|
+
description: "Additional time intervals for comparison (e.g., ['1h_ago', '1d_ago'])",
|
|
309
|
+
items: { type: "string" },
|
|
310
|
+
},
|
|
311
|
+
add_on_data_models: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "Comma-separated additional data models to join",
|
|
314
|
+
},
|
|
315
|
+
time_shift: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Time shift for comparison (e.g., '1h', '1d', '1w')",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["metric_name"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "vunet_get_status",
|
|
325
|
+
description: "Get the current connection status and tenant information",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "vunet_list_data_models",
|
|
333
|
+
description: "List common Vunet data models organized by category (APM, Infrastructure, Database, Network, etc.)",
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
category: {
|
|
338
|
+
type: "string",
|
|
339
|
+
description: "Filter by category: 'apm', 'infrastructure', 'database', 'network', 'business', 'all' (default: 'all')",
|
|
340
|
+
enum: ["all", "apm", "infrastructure", "database", "network", "business", "logs", "security"],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
// Handle tool calls
|
|
349
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
350
|
+
const { name, arguments: args } = request.params;
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
switch (name) {
|
|
354
|
+
case "vunet_query_metric":
|
|
355
|
+
return await this.handleQueryMetric(args);
|
|
356
|
+
|
|
357
|
+
case "vunet_get_status":
|
|
358
|
+
return await this.handleGetStatus();
|
|
359
|
+
|
|
360
|
+
case "vunet_list_data_models":
|
|
361
|
+
return await this.handleListDataModels(args);
|
|
362
|
+
|
|
363
|
+
default:
|
|
364
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: `Error: ${error.message}`,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
isError: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async handleQueryMetric(args) {
|
|
381
|
+
const { metric_name, ...queryArgs } = args;
|
|
382
|
+
|
|
383
|
+
if (!metric_name) {
|
|
384
|
+
throw new Error("metric_name is required");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const queryParams = this.buildQueryParams(queryArgs);
|
|
388
|
+
const result = await this.queryMetric(metric_name, queryParams);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: JSON.stringify(result, null, 2),
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async handleGetStatus() {
|
|
401
|
+
const status = {
|
|
402
|
+
configured: !!(this.tenantUrl && this.username && this.password),
|
|
403
|
+
tenant_url: this.tenantUrl || "Not configured",
|
|
404
|
+
bu_id: this.buId,
|
|
405
|
+
authenticated: this.isAuthenticated,
|
|
406
|
+
session_valid: this.isAuthenticated && this.sessionExpiry && Date.now() < this.sessionExpiry,
|
|
407
|
+
session_expires_in: this.sessionExpiry ? Math.max(0, this.sessionExpiry - Date.now()) / 1000 : 0,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: "text",
|
|
414
|
+
text: JSON.stringify(status, null, 2),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async handleListDataModels(args = {}) {
|
|
421
|
+
const category = args.category || "all";
|
|
422
|
+
|
|
423
|
+
const dataModels = {
|
|
424
|
+
apm: [
|
|
425
|
+
"Trace Map Data",
|
|
426
|
+
"Traces Transaction Volume",
|
|
427
|
+
"Application Response Time",
|
|
428
|
+
"Error Rate",
|
|
429
|
+
"Service Dependencies",
|
|
430
|
+
"Endpoint Performance",
|
|
431
|
+
"JVM Metrics",
|
|
432
|
+
"Thread Pool Metrics",
|
|
433
|
+
"GC Metrics",
|
|
434
|
+
"Hibernate Query Performance",
|
|
435
|
+
"Cache Hit Ratio"
|
|
436
|
+
],
|
|
437
|
+
infrastructure: [
|
|
438
|
+
"Kubernetes Pod Metrics",
|
|
439
|
+
"Kubernetes Node Metrics",
|
|
440
|
+
"Kubernetes Deployment Metrics",
|
|
441
|
+
"Kubernetes Service Metrics",
|
|
442
|
+
"Kubernetes Namespace Metrics",
|
|
443
|
+
"Container Metrics",
|
|
444
|
+
"Kubernetes Events",
|
|
445
|
+
"CPU Usage",
|
|
446
|
+
"Memory Usage",
|
|
447
|
+
"Disk I/O",
|
|
448
|
+
"Network I/O",
|
|
449
|
+
"File System Usage",
|
|
450
|
+
"System Load",
|
|
451
|
+
"Apache Metrics",
|
|
452
|
+
"Nginx Metrics",
|
|
453
|
+
"Tomcat Metrics",
|
|
454
|
+
"IIS Metrics"
|
|
455
|
+
],
|
|
456
|
+
database: [
|
|
457
|
+
"MySQL Performance",
|
|
458
|
+
"MySQL Slow Queries",
|
|
459
|
+
"PostgreSQL Metrics",
|
|
460
|
+
"Oracle Metrics",
|
|
461
|
+
"SQL Server Metrics",
|
|
462
|
+
"Database Connection Pool",
|
|
463
|
+
"MongoDB Metrics",
|
|
464
|
+
"Redis Metrics",
|
|
465
|
+
"Cassandra Metrics",
|
|
466
|
+
"Elasticsearch Metrics"
|
|
467
|
+
],
|
|
468
|
+
network: [
|
|
469
|
+
"Network Latency",
|
|
470
|
+
"Packet Loss",
|
|
471
|
+
"Bandwidth Usage",
|
|
472
|
+
"TCP Connections",
|
|
473
|
+
"DNS Query Performance",
|
|
474
|
+
"SSL Certificate Expiry"
|
|
475
|
+
],
|
|
476
|
+
business: [
|
|
477
|
+
"VuBank Metrics",
|
|
478
|
+
"Transaction Success Rate",
|
|
479
|
+
"User Sessions",
|
|
480
|
+
"Revenue Metrics",
|
|
481
|
+
"Conversion Rate",
|
|
482
|
+
"Customer Activity"
|
|
483
|
+
],
|
|
484
|
+
logs: [
|
|
485
|
+
"Application Logs",
|
|
486
|
+
"Error Logs",
|
|
487
|
+
"Security Logs",
|
|
488
|
+
"Audit Logs",
|
|
489
|
+
"Access Logs"
|
|
490
|
+
],
|
|
491
|
+
security: [
|
|
492
|
+
"Security Events",
|
|
493
|
+
"Failed Login Attempts",
|
|
494
|
+
"Privileged Access",
|
|
495
|
+
"Vulnerability Scan Results",
|
|
496
|
+
"Compliance Metrics"
|
|
497
|
+
]
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
let result;
|
|
501
|
+
if (category === "all") {
|
|
502
|
+
result = {
|
|
503
|
+
message: "Common Vunet data models by category (80+ total)",
|
|
504
|
+
note: "Actual available models depend on your tenant configuration",
|
|
505
|
+
categories: dataModels,
|
|
506
|
+
total_listed: Object.values(dataModels).flat().length,
|
|
507
|
+
usage: "Use these names with the vunet_query_metric tool"
|
|
508
|
+
};
|
|
509
|
+
} else if (dataModels[category]) {
|
|
510
|
+
result = {
|
|
511
|
+
category: category.toUpperCase(),
|
|
512
|
+
models: dataModels[category],
|
|
513
|
+
count: dataModels[category].length,
|
|
514
|
+
usage: "Use these names with the vunet_query_metric tool"
|
|
515
|
+
};
|
|
516
|
+
} else {
|
|
517
|
+
result = {
|
|
518
|
+
error: `Unknown category: ${category}`,
|
|
519
|
+
available_categories: Object.keys(dataModels),
|
|
520
|
+
usage: "Specify 'all' or one of the available categories"
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: "text",
|
|
528
|
+
text: JSON.stringify(result, null, 2),
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async run() {
|
|
535
|
+
console.error("Vunet MCP Server starting...");
|
|
536
|
+
|
|
537
|
+
// Log configuration status
|
|
538
|
+
if (this.tenantUrl && this.username && this.password) {
|
|
539
|
+
console.error(`[Vunet MCP] Configured for tenant: ${this.tenantUrl}`);
|
|
540
|
+
console.error(`[Vunet MCP] Business Unit ID: ${this.buId}`);
|
|
541
|
+
console.error(`[Vunet MCP] SSL Verification: ${this.httpsAgent.options.rejectUnauthorized ? 'Enabled' : 'Disabled'}`);
|
|
542
|
+
} else {
|
|
543
|
+
console.error(`[Vunet MCP] WARNING: Tenant not configured!`);
|
|
544
|
+
console.error(`[Vunet MCP] Set VUNET_TENANT_URL, VUNET_USERNAME, and VUNET_PASSWORD environment variables`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const transport = new StdioServerTransport();
|
|
548
|
+
await this.server.connect(transport);
|
|
549
|
+
console.error("Vunet MCP Server running on stdio");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Start the server
|
|
554
|
+
const server = new VunetMCPServer();
|
|
555
|
+
server.run().catch((error) => {
|
|
556
|
+
console.error("Fatal error:", error);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"vunet-production": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": [
|
|
6
|
+
"@vunet/mcp-server"
|
|
7
|
+
],
|
|
8
|
+
"env": {
|
|
9
|
+
"VUNET_TENANT_URL": "https://your-production-tenant.com",
|
|
10
|
+
"VUNET_USERNAME": "admin",
|
|
11
|
+
"VUNET_PASSWORD": "your-secure-password",
|
|
12
|
+
"VUNET_BU_ID": "1",
|
|
13
|
+
"VUNET_VERIFY_SSL": "true"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"vunet-staging": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": [
|
|
19
|
+
"@vunet/mcp-server"
|
|
20
|
+
],
|
|
21
|
+
"env": {
|
|
22
|
+
"VUNET_TENANT_URL": "https://staging-tenant.com",
|
|
23
|
+
"VUNET_BEARER_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
24
|
+
"VUNET_BU_ID": "1",
|
|
25
|
+
"VUNET_VERIFY_SSL": "true"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"vunet-dev": {
|
|
29
|
+
"command": "node",
|
|
30
|
+
"args": [
|
|
31
|
+
"C:\\path\\to\\vunet-mcp-server\\index.js"
|
|
32
|
+
],
|
|
33
|
+
"env": {
|
|
34
|
+
"VUNET_TENANT_URL": "https://20.198.26.207",
|
|
35
|
+
"VUNET_USERNAME": "vunetadmin",
|
|
36
|
+
"VUNET_PASSWORD": "Qwerty@123",
|
|
37
|
+
"VUNET_BU_ID": "1",
|
|
38
|
+
"VUNET_VERIFY_SSL": "false"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mithung/vunet-mcp-server",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Model Context Protocol (MCP) Server for Vunet vuSmartMaps - Multi-tenant observability platform integration",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vunet-mcp": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"dev": "node --watch index.js",
|
|
13
|
+
"test": "echo \"No tests specified yet\" && exit 0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"vunet",
|
|
19
|
+
"vusmartmaps",
|
|
20
|
+
"metric-api",
|
|
21
|
+
"monitoring",
|
|
22
|
+
"observability",
|
|
23
|
+
"analytics",
|
|
24
|
+
"apm",
|
|
25
|
+
"traces",
|
|
26
|
+
"metrics",
|
|
27
|
+
"logs",
|
|
28
|
+
"dynatrace-alternative"
|
|
29
|
+
],
|
|
30
|
+
"author": {
|
|
31
|
+
"name": "Vunet Systems",
|
|
32
|
+
"url": "https://vunetsystems.com"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/mithung/vunet-mcp-server.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/mithung/vunet-mcp-server/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/mithung/vunet-mcp-server#readme",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
45
|
+
"axios": "^1.7.2"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"index.js",
|
|
53
|
+
"README.md",
|
|
54
|
+
"SETUP.md",
|
|
55
|
+
"QUICKSTART.md",
|
|
56
|
+
"DATA_MODELS.md",
|
|
57
|
+
"CHANGELOG.md",
|
|
58
|
+
"LICENSE",
|
|
59
|
+
"config.example.json",
|
|
60
|
+
"mcp-config.example.json"
|
|
61
|
+
]
|
|
62
|
+
}
|