@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/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
+ }