mcp-spring-boot-actuator 0.1.2 → 0.1.4

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 CHANGED
@@ -199,7 +199,7 @@ This server works alongside:
199
199
  - **Custom health indicators**: The tool recognizes standard health indicator patterns. Custom health indicators with non-standard status values may not trigger specific recommendations.
200
200
  - **Cache analysis**: Supports ConcurrentMapCache, Caffeine, Redis, and EhCache. Other cache providers may show limited analysis.
201
201
  - **Non-JSON responses**: Handles HTML error pages (401, 403, 500) gracefully with "Invalid JSON" warnings, but cannot extract useful data from them.
202
- - **Circular dependency depth**: Detects A→B→A cycles but may miss longer chains (A→B→C→A) in complex applications.
202
+ - **Circular dependency depth**: Detects cycles of any length, including multi-hop chains (A→B→C→A).
203
203
 
204
204
  ## License
205
205
 
package/build/index.js CHANGED
@@ -8,9 +8,15 @@
8
8
  * analyze_env — Detect exposed secrets and risky configurations
9
9
  * analyze_beans — Detect circular dependencies and scope mismatches
10
10
  */
11
+ import { readFileSync } from "fs";
12
+ import { fileURLToPath } from "url";
13
+ import { dirname, join } from "path";
11
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
16
  import { z } from "zod";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
14
20
  import { parseHealth } from "./parsers/health.js";
15
21
  import { analyzeMetrics } from "./analyzers/metrics.js";
16
22
  import { analyzeEnv } from "./analyzers/env-risk.js";
@@ -18,13 +24,10 @@ import { analyzeBeans } from "./analyzers/beans.js";
18
24
  import { analyzeStartup } from "./analyzers/startup.js";
19
25
  import { analyzeCaches } from "./analyzers/caches.js";
20
26
  import { analyzeLoggers } from "./analyzers/loggers.js";
21
- import { validateLicense, formatUpgradePrompt } from "./license.js";
22
27
  import { formatSeveritySummary } from "./format.js";
23
- // License check (reads MCP_LICENSE_KEY env var once at startup)
24
- const license = validateLicense(process.env.MCP_LICENSE_KEY, "spring-boot-actuator");
25
28
  // Handle --help
26
29
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
27
- console.log(`mcp-spring-boot-actuator v0.1.1 — MCP server for Spring Boot Actuator diagnostics
30
+ console.log(`mcp-spring-boot-actuator v${pkg.version} — MCP server for Spring Boot Actuator diagnostics
28
31
 
29
32
  Usage:
30
33
  mcp-spring-boot-actuator [options]
@@ -44,245 +47,269 @@ Tools provided:
44
47
  }
45
48
  const server = new McpServer({
46
49
  name: "mcp-spring-boot-actuator",
47
- version: "0.1.0",
50
+ version: pkg.version,
48
51
  });
49
52
  // Tool 1: analyze_health
50
53
  server.tool("analyze_health", "Analyze a Spring Boot Actuator /health endpoint response. Diagnoses unhealthy components and provides recommendations.", {
51
54
  json: z.string().describe("JSON response from the /health endpoint (curl http://localhost:8080/actuator/health)"),
52
55
  }, async ({ json }) => {
53
- const report = parseHealth(json);
54
- let output = `## Health Analysis\n\n`;
55
- output += `**Overall Status**: ${report.overallStatus}\n`;
56
- output += `**Components**: ${report.components.length}\n\n`;
57
- if (report.components.length > 0) {
58
- output += "### Component Status\n\n";
59
- output += "| Component | Status | Details |\n|-----------|--------|--------|\n";
60
- for (const comp of report.components) {
61
- const details = Object.entries(comp.details).slice(0, 3).map(([k, v]) => `${k}=${v}`).join(", ");
62
- output += `| ${comp.name} | ${comp.status} | ${details || "-"} |\n`;
56
+ try {
57
+ const report = parseHealth(json);
58
+ let output = `## Health Analysis\n\n`;
59
+ output += `**Overall Status**: ${report.overallStatus}\n`;
60
+ output += `**Components**: ${report.components.length}\n\n`;
61
+ if (report.components.length > 0) {
62
+ output += "### Component Status\n\n";
63
+ output += "| Component | Status | Details |\n|-----------|--------|--------|\n";
64
+ for (const comp of report.components) {
65
+ const details = Object.entries(comp.details).slice(0, 3).map(([k, v]) => `${k}=${v}`).join(", ");
66
+ output += `| ${comp.name} | ${comp.status} | ${details || "-"} |\n`;
67
+ }
68
+ output += "\n";
63
69
  }
64
- output += "\n";
65
- }
66
- if (report.issues.length > 0) {
67
- output += "### Issues\n\n";
68
- for (const issue of report.issues) {
69
- output += `**${issue.severity}** [${issue.component}]: ${issue.message}\n\n`;
70
+ if (report.issues.length > 0) {
71
+ output += "### Issues\n\n";
72
+ for (const issue of report.issues) {
73
+ output += `**${issue.severity}** [${issue.component}]: ${issue.message}\n\n`;
74
+ }
70
75
  }
71
- }
72
- if (report.recommendations.length > 0) {
73
- output += "### Recommendations\n\n";
74
- for (const rec of report.recommendations) {
75
- output += `- ${rec}\n`;
76
+ if (report.recommendations.length > 0) {
77
+ output += "### Recommendations\n\n";
78
+ for (const rec of report.recommendations) {
79
+ output += `- ${rec}\n`;
80
+ }
76
81
  }
82
+ output += formatSeveritySummary(report.issues);
83
+ return { content: [{ type: "text", text: output }] };
84
+ }
85
+ catch (err) {
86
+ return {
87
+ content: [{
88
+ type: "text",
89
+ text: `Error analyzing health data: ${err instanceof Error ? err.message : String(err)}`,
90
+ }],
91
+ };
77
92
  }
78
- output += formatSeveritySummary(report.issues);
79
- return { content: [{ type: "text", text: output }] };
80
93
  });
81
94
  // Tool 2: analyze_metrics
82
95
  server.tool("analyze_metrics", "Analyze Spring Boot Actuator metrics data. Detects JVM memory pressure, high error rates, connection pool exhaustion, and GC issues.", {
83
96
  json: z.string().describe("JSON object with metric names as keys and values (e.g., from /metrics endpoints)"),
84
97
  }, async ({ json }) => {
85
- const report = analyzeMetrics(json);
86
- let output = `## Metrics Analysis\n\n`;
87
- if (report.jvm) {
88
- output += "### JVM\n\n";
89
- output += `| Metric | Value |\n|--------|-------|\n`;
90
- output += `| Heap Used | ${formatBytes(report.jvm.heapUsed)} |\n`;
91
- output += `| Heap Max | ${formatBytes(report.jvm.heapMax)} |\n`;
92
- output += `| Heap Utilization | ${(report.jvm.heapUtilization * 100).toFixed(1)}% |\n`;
93
- output += `| Thread Count | ${report.jvm.threadCount} |\n`;
94
- output += `| Thread Peak | ${report.jvm.threadPeak} |\n`;
95
- output += `| GC Pauses | ${report.jvm.gcPauseCount} (${report.jvm.gcPauseTotal.toFixed(1)}s total) |\n`;
96
- output += `| Loaded Classes | ${report.jvm.loadedClasses} |\n\n`;
97
- }
98
- if (report.http) {
99
- output += "### HTTP\n\n";
100
- output += `| Metric | Value |\n|--------|-------|\n`;
101
- output += `| Total Requests | ${report.http.totalRequests} |\n`;
102
- output += `| Error Rate | ${(report.http.errorRate * 100).toFixed(1)}% |\n`;
103
- output += `| Max Latency | ${report.http.maxLatency}ms |\n\n`;
104
- }
105
- if (report.issues.length > 0) {
106
- output += "### Issues\n\n";
107
- for (const issue of report.issues) {
108
- output += `**${issue.severity}** [${issue.category}]: ${issue.message}\n\n`;
98
+ try {
99
+ const report = analyzeMetrics(json);
100
+ let output = `## Metrics Analysis\n\n`;
101
+ if (report.jvm) {
102
+ output += "### JVM\n\n";
103
+ output += `| Metric | Value |\n|--------|-------|\n`;
104
+ output += `| Heap Used | ${formatBytes(report.jvm.heapUsed)} |\n`;
105
+ output += `| Heap Max | ${formatBytes(report.jvm.heapMax)} |\n`;
106
+ output += `| Heap Utilization | ${(report.jvm.heapUtilization * 100).toFixed(1)}% |\n`;
107
+ output += `| Thread Count | ${report.jvm.threadCount} |\n`;
108
+ output += `| Thread Peak | ${report.jvm.threadPeak} |\n`;
109
+ output += `| GC Pauses | ${report.jvm.gcPauseCount} (${report.jvm.gcPauseTotal.toFixed(1)}s total) |\n`;
110
+ output += `| Loaded Classes | ${report.jvm.loadedClasses} |\n\n`;
109
111
  }
110
- }
111
- if (report.recommendations.length > 0) {
112
- output += "### Recommendations\n\n";
113
- for (const rec of report.recommendations) {
114
- output += `- ${rec}\n`;
112
+ if (report.http) {
113
+ output += "### HTTP\n\n";
114
+ output += `| Metric | Value |\n|--------|-------|\n`;
115
+ output += `| Total Requests | ${report.http.totalRequests} |\n`;
116
+ output += `| Error Rate | ${(report.http.errorRate * 100).toFixed(1)}% |\n`;
117
+ output += `| Max Latency | ${report.http.maxLatency}ms |\n\n`;
115
118
  }
119
+ if (report.issues.length > 0) {
120
+ output += "### Issues\n\n";
121
+ for (const issue of report.issues) {
122
+ output += `**${issue.severity}** [${issue.category}]: ${issue.message}\n\n`;
123
+ }
124
+ }
125
+ if (report.recommendations.length > 0) {
126
+ output += "### Recommendations\n\n";
127
+ for (const rec of report.recommendations) {
128
+ output += `- ${rec}\n`;
129
+ }
130
+ }
131
+ if (!report.jvm && !report.http && report.issues.length === 0) {
132
+ output += "No recognized metrics found. Provide metrics in the format: `{\"jvm.memory.used\": 1234567, ...}`\n";
133
+ }
134
+ output += formatSeveritySummary(report.issues);
135
+ return { content: [{ type: "text", text: output }] };
116
136
  }
117
- if (!report.jvm && !report.http && report.issues.length === 0) {
118
- output += "No recognized metrics found. Provide metrics in the format: `{\"jvm.memory.used\": 1234567, ...}`\n";
137
+ catch (err) {
138
+ return {
139
+ content: [{
140
+ type: "text",
141
+ text: `Error analyzing metrics data: ${err instanceof Error ? err.message : String(err)}`,
142
+ }],
143
+ };
119
144
  }
120
- output += formatSeveritySummary(report.issues);
121
- return { content: [{ type: "text", text: output }] };
122
145
  });
123
146
  // Tool 3: analyze_env
124
147
  server.tool("analyze_env", "Analyze Spring Boot Actuator /env endpoint response. Detects exposed secrets, risky configurations, and missing production settings.", {
125
148
  json: z.string().describe("JSON response from the /env endpoint (curl http://localhost:8080/actuator/env)"),
126
149
  }, async ({ json }) => {
127
- const report = analyzeEnv(json);
128
- let output = `## Environment Analysis\n\n`;
129
- output += `**Active Profiles**: ${report.activeProfiles.length > 0 ? report.activeProfiles.join(", ") : "none"}\n`;
130
- output += `**Property Sources**: ${report.propertySources.length}\n\n`;
131
- if (report.risks.length > 0) {
132
- output += "### Risks\n\n";
133
- const critical = report.risks.filter(r => r.severity === "CRITICAL");
134
- const warning = report.risks.filter(r => r.severity === "WARNING");
135
- const info = report.risks.filter(r => r.severity === "INFO");
136
- for (const group of [critical, warning, info]) {
137
- for (const risk of group) {
138
- output += `**${risk.severity}** \`${risk.property}\`: ${risk.message}\n`;
139
- output += `> ${risk.recommendation}\n\n`;
150
+ try {
151
+ const report = analyzeEnv(json);
152
+ let output = `## Environment Analysis\n\n`;
153
+ output += `**Active Profiles**: ${report.activeProfiles.length > 0 ? report.activeProfiles.join(", ") : "none"}\n`;
154
+ output += `**Property Sources**: ${report.propertySources.length}\n\n`;
155
+ if (report.risks.length > 0) {
156
+ output += "### Risks\n\n";
157
+ const critical = report.risks.filter(r => r.severity === "CRITICAL");
158
+ const warning = report.risks.filter(r => r.severity === "WARNING");
159
+ const info = report.risks.filter(r => r.severity === "INFO");
160
+ for (const group of [critical, warning, info]) {
161
+ for (const risk of group) {
162
+ output += `**${risk.severity}** \`${risk.property}\`: ${risk.message}\n`;
163
+ output += `> ${risk.recommendation}\n\n`;
164
+ }
140
165
  }
141
166
  }
142
- }
143
- else {
144
- output += "### No risks detected.\n\n";
145
- }
146
- if (report.recommendations.length > 0) {
147
- output += "### Recommendations\n\n";
148
- for (const rec of report.recommendations) {
149
- output += `- ${rec}\n`;
167
+ else {
168
+ output += "### No risks detected.\n\n";
169
+ }
170
+ if (report.recommendations.length > 0) {
171
+ output += "### Recommendations\n\n";
172
+ for (const rec of report.recommendations) {
173
+ output += `- ${rec}\n`;
174
+ }
150
175
  }
176
+ output += formatSeveritySummary(report.risks);
177
+ return { content: [{ type: "text", text: output }] };
178
+ }
179
+ catch (err) {
180
+ return {
181
+ content: [{
182
+ type: "text",
183
+ text: `Error analyzing environment data: ${err instanceof Error ? err.message : String(err)}`,
184
+ }],
185
+ };
151
186
  }
152
- output += formatSeveritySummary(report.risks);
153
- return { content: [{ type: "text", text: output }] };
154
187
  });
155
188
  // Tool 4: analyze_beans
156
189
  server.tool("analyze_beans", "Analyze Spring Boot Actuator /beans endpoint response. Detects circular dependencies, scope mismatches, and bean architecture issues.", {
157
190
  json: z.string().describe("JSON response from the /beans endpoint (curl http://localhost:8080/actuator/beans)"),
158
191
  }, async ({ json }) => {
159
- if (!license.isPro) {
192
+ try {
193
+ const report = analyzeBeans(json);
194
+ let output = `## Bean Analysis\n\n`;
195
+ output += `**Total Beans**: ${report.totalBeans}\n`;
196
+ if (report.contexts.length > 0) {
197
+ output += `**Contexts**: ${report.contexts.join(", ")}\n`;
198
+ }
199
+ output += "\n";
200
+ if (report.issues.length > 0) {
201
+ output += "### Issues\n\n";
202
+ for (const issue of report.issues) {
203
+ output += `**${issue.severity}**: ${issue.message}\n`;
204
+ if (issue.beans.length > 0) {
205
+ output += `> Beans: ${issue.beans.join(", ")}\n`;
206
+ }
207
+ output += "\n";
208
+ }
209
+ }
210
+ else {
211
+ output += "### No issues detected.\n\n";
212
+ }
213
+ if (report.recommendations.length > 0) {
214
+ output += "### Recommendations\n\n";
215
+ for (const rec of report.recommendations) {
216
+ output += `- ${rec}\n`;
217
+ }
218
+ }
219
+ output += formatSeveritySummary(report.issues);
220
+ return { content: [{ type: "text", text: output }] };
221
+ }
222
+ catch (err) {
160
223
  return {
161
224
  content: [{
162
225
  type: "text",
163
- text: formatUpgradePrompt("analyze_beans", "Bean architecture analysis with:\n" +
164
- "- Circular dependency detection\n" +
165
- "- Scope mismatch identification\n" +
166
- "- Bean dependency graph analysis\n" +
167
- "- Context hierarchy issues"),
226
+ text: `Error analyzing beans data: ${err instanceof Error ? err.message : String(err)}`,
168
227
  }],
169
228
  };
170
229
  }
171
- const report = analyzeBeans(json);
172
- let output = `## Bean Analysis\n\n`;
173
- output += `**Total Beans**: ${report.totalBeans}\n`;
174
- if (report.contexts.length > 0) {
175
- output += `**Contexts**: ${report.contexts.join(", ")}\n`;
176
- }
177
- output += "\n";
178
- if (report.issues.length > 0) {
179
- output += "### Issues\n\n";
180
- for (const issue of report.issues) {
181
- output += `**${issue.severity}**: ${issue.message}\n`;
182
- if (issue.beans.length > 0) {
183
- output += `> Beans: ${issue.beans.join(", ")}\n`;
184
- }
185
- output += "\n";
186
- }
187
- }
188
- else {
189
- output += "### No issues detected.\n\n";
190
- }
191
- if (report.recommendations.length > 0) {
192
- output += "### Recommendations\n\n";
193
- for (const rec of report.recommendations) {
194
- output += `- ${rec}\n`;
195
- }
196
- }
197
- output += formatSeveritySummary(report.issues);
198
- return { content: [{ type: "text", text: output }] };
199
230
  });
200
231
  // Tool 5: analyze_startup
201
232
  server.tool("analyze_startup", "Analyze Spring Boot Actuator /startup endpoint (Spring Boot 3.2+). Detects slow bean initialization, heavy auto-configurations, and startup bottlenecks.", {
202
233
  json: z.string().describe("JSON response from the /startup endpoint (curl http://localhost:8080/actuator/startup)"),
203
234
  }, async ({ json }) => {
204
- if (!license.isPro) {
235
+ try {
236
+ const report = analyzeStartup(json);
237
+ let output = `## Startup Analysis\n\n`;
238
+ output += `**Total Startup Time**: ${(report.totalDurationMs / 1000).toFixed(1)}s\n`;
239
+ output += `**Startup Steps**: ${report.steps.length}\n`;
240
+ output += `**Slow Steps**: ${report.slowSteps.length}\n\n`;
241
+ if (report.slowSteps.length > 0) {
242
+ output += "### Slowest Steps\n\n";
243
+ output += "| Step | Duration | Bean |\n|------|----------|------|\n";
244
+ for (const step of report.slowSteps.slice(0, 15)) {
245
+ const bean = step.tags.beanName || "-";
246
+ output += `| ${step.name} | ${(step.durationMs / 1000).toFixed(2)}s | ${bean} |\n`;
247
+ }
248
+ output += "\n";
249
+ }
250
+ if (report.issues.length > 0) {
251
+ output += "### Issues\n\n";
252
+ for (const issue of report.issues) {
253
+ output += `**${issue.severity}**: ${issue.message}\n\n`;
254
+ }
255
+ }
256
+ if (report.recommendations.length > 0) {
257
+ output += "### Recommendations\n\n";
258
+ for (const rec of report.recommendations) {
259
+ output += `- ${rec}\n`;
260
+ }
261
+ }
262
+ output += formatSeveritySummary(report.issues);
263
+ return { content: [{ type: "text", text: output }] };
264
+ }
265
+ catch (err) {
205
266
  return {
206
267
  content: [{
207
268
  type: "text",
208
- text: formatUpgradePrompt("analyze_startup", "Startup performance analysis with:\n" +
209
- "- Slow bean initialization detection\n" +
210
- "- Heavy auto-configuration identification\n" +
211
- "- Startup bottleneck analysis\n" +
212
- "- Bean init time ranking"),
269
+ text: `Error analyzing startup data: ${err instanceof Error ? err.message : String(err)}`,
213
270
  }],
214
271
  };
215
272
  }
216
- const report = analyzeStartup(json);
217
- let output = `## Startup Analysis\n\n`;
218
- output += `**Total Startup Time**: ${(report.totalDurationMs / 1000).toFixed(1)}s\n`;
219
- output += `**Startup Steps**: ${report.steps.length}\n`;
220
- output += `**Slow Steps**: ${report.slowSteps.length}\n\n`;
221
- if (report.slowSteps.length > 0) {
222
- output += "### Slowest Steps\n\n";
223
- output += "| Step | Duration | Bean |\n|------|----------|------|\n";
224
- for (const step of report.slowSteps.slice(0, 15)) {
225
- const bean = step.tags.beanName || "-";
226
- output += `| ${step.name} | ${(step.durationMs / 1000).toFixed(2)}s | ${bean} |\n`;
227
- }
228
- output += "\n";
229
- }
230
- if (report.issues.length > 0) {
231
- output += "### Issues\n\n";
232
- for (const issue of report.issues) {
233
- output += `**${issue.severity}**: ${issue.message}\n\n`;
234
- }
235
- }
236
- if (report.recommendations.length > 0) {
237
- output += "### Recommendations\n\n";
238
- for (const rec of report.recommendations) {
239
- output += `- ${rec}\n`;
240
- }
241
- }
242
- output += formatSeveritySummary(report.issues);
243
- return { content: [{ type: "text", text: output }] };
244
273
  });
245
274
  // Tool 6: analyze_caches
246
275
  server.tool("analyze_caches", "Analyze Spring Boot Actuator /caches endpoint. Lists registered caches, detects unbounded caches (memory leak risk), and identifies cache configuration issues.", {
247
276
  json: z.string().describe("JSON response from the /caches endpoint (curl http://localhost:8080/actuator/caches)"),
248
277
  }, async ({ json }) => {
249
- if (!license.isPro) {
278
+ try {
279
+ const report = analyzeCaches(json);
280
+ let output = `## Cache Analysis\n\n`;
281
+ output += `**Registered Caches**: ${report.caches.length}\n\n`;
282
+ if (report.caches.length > 0) {
283
+ output += "### Cache Registry\n\n";
284
+ output += "| Cache | Manager | Implementation |\n|-------|---------|----------------|\n";
285
+ for (const cache of report.caches) {
286
+ output += `| ${cache.name} | ${cache.cacheManager} | ${cache.target} |\n`;
287
+ }
288
+ output += "\n";
289
+ }
290
+ if (report.issues.length > 0) {
291
+ output += "### Issues\n\n";
292
+ for (const issue of report.issues) {
293
+ output += `**${issue.severity}**${issue.cache ? ` [${issue.cache}]` : ""}: ${issue.message}\n\n`;
294
+ }
295
+ }
296
+ if (report.recommendations.length > 0) {
297
+ output += "### Recommendations\n\n";
298
+ for (const rec of report.recommendations) {
299
+ output += `- ${rec}\n`;
300
+ }
301
+ }
302
+ output += formatSeveritySummary(report.issues);
303
+ return { content: [{ type: "text", text: output }] };
304
+ }
305
+ catch (err) {
250
306
  return {
251
307
  content: [{
252
308
  type: "text",
253
- text: formatUpgradePrompt("analyze_caches", "Cache health analysis with:\n" +
254
- "- Unbounded cache detection (memory leak risk)\n" +
255
- "- Cache configuration audit\n" +
256
- "- Cache manager inventory\n" +
257
- "- Optimization recommendations"),
309
+ text: `Error analyzing cache data: ${err instanceof Error ? err.message : String(err)}`,
258
310
  }],
259
311
  };
260
312
  }
261
- const report = analyzeCaches(json);
262
- let output = `## Cache Analysis\n\n`;
263
- output += `**Registered Caches**: ${report.caches.length}\n\n`;
264
- if (report.caches.length > 0) {
265
- output += "### Cache Registry\n\n";
266
- output += "| Cache | Manager | Implementation |\n|-------|---------|----------------|\n";
267
- for (const cache of report.caches) {
268
- output += `| ${cache.name} | ${cache.cacheManager} | ${cache.target} |\n`;
269
- }
270
- output += "\n";
271
- }
272
- if (report.issues.length > 0) {
273
- output += "### Issues\n\n";
274
- for (const issue of report.issues) {
275
- output += `**${issue.severity}**${issue.cache ? ` [${issue.cache}]` : ""}: ${issue.message}\n\n`;
276
- }
277
- }
278
- if (report.recommendations.length > 0) {
279
- output += "### Recommendations\n\n";
280
- for (const rec of report.recommendations) {
281
- output += `- ${rec}\n`;
282
- }
283
- }
284
- output += formatSeveritySummary(report.issues);
285
- return { content: [{ type: "text", text: output }] };
286
313
  });
287
314
  // Tool 7: analyze_loggers
288
315
  server.tool("analyze_loggers", "Analyze Spring Boot /loggers endpoint response. Detects DEBUG/TRACE in production, inconsistent log levels across packages, and verbose framework logging.", {
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "mcp-spring-boot-actuator",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "MCP server for Spring Boot Actuator analysis — health, metrics, environment, and bean diagnostics",
5
+ "mcpName": "Spring Boot Actuator",
5
6
  "main": "build/index.js",
6
7
  "bin": {
7
8
  "mcp-spring-boot-actuator": "build/index.js"
package/build/license.js DELETED
@@ -1,115 +0,0 @@
1
- /**
2
- * License validation for MCP Migration Advisor (Pro features).
3
- *
4
- * Validates license keys offline using HMAC-SHA256.
5
- * Missing or invalid keys gracefully degrade to free mode — never errors.
6
- *
7
- * Key format: MCPJBS-XXXXX-XXXXX-XXXXX-XXXXX
8
- * Payload (12 bytes = 20 base32 chars):
9
- * [0] product mask (8 bits)
10
- * [1-2] expiry days since 2026-01-01 (16 bits)
11
- * [3-5] customer ID (24 bits)
12
- * [6-11] HMAC-SHA256 truncated (48 bits)
13
- */
14
- import { createHmac } from "node:crypto";
15
- const KEY_PREFIX = "MCPJBS-";
16
- const EPOCH = new Date("2026-01-01T00:00:00Z");
17
- const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
18
- const HMAC_SECRET = "mcp-java-backend-suite-license-v1";
19
- const PRODUCTS = {
20
- "db-analyzer": 0,
21
- "jvm-diagnostics": 1,
22
- "migration-advisor": 2,
23
- "spring-boot-actuator": 3,
24
- "redis-diagnostics": 4,
25
- };
26
- export function validateLicense(key, product) {
27
- const FREE = {
28
- isPro: false,
29
- expiresAt: null,
30
- customerId: null,
31
- reason: "No license key provided",
32
- };
33
- if (!key || key.trim().length === 0)
34
- return FREE;
35
- const trimmed = key.trim().toUpperCase();
36
- if (!trimmed.startsWith(KEY_PREFIX)) {
37
- return { ...FREE, reason: "Invalid key format: missing MCPJBS- prefix" };
38
- }
39
- const body = trimmed.slice(KEY_PREFIX.length).replace(/-/g, "");
40
- if (body.length < 20) {
41
- return { ...FREE, reason: "Invalid key format: too short" };
42
- }
43
- let decoded;
44
- try {
45
- decoded = base32Decode(body.slice(0, 20));
46
- }
47
- catch {
48
- return { ...FREE, reason: "Invalid key format: bad base32 encoding" };
49
- }
50
- if (decoded.length < 12) {
51
- return { ...FREE, reason: "Invalid key format: decoded data too short" };
52
- }
53
- const payload = decoded.subarray(0, 6);
54
- const providedSignature = decoded.subarray(6, 12);
55
- const expectedHmac = createHmac("sha256", HMAC_SECRET)
56
- .update(payload)
57
- .digest();
58
- const expectedSignature = expectedHmac.subarray(0, 6);
59
- if (!providedSignature.equals(expectedSignature)) {
60
- return { ...FREE, reason: "Invalid license key: signature mismatch" };
61
- }
62
- const productMask = payload[0];
63
- const daysSinceEpoch = (payload[1] << 8) | payload[2];
64
- const customerId = (payload[3] << 16) | (payload[4] << 8) | payload[5];
65
- const productBit = PRODUCTS[product];
66
- if (productBit === undefined) {
67
- return { ...FREE, reason: `Unknown product: ${product}` };
68
- }
69
- if ((productMask & (1 << productBit)) === 0) {
70
- return { ...FREE, customerId, reason: `License does not include ${product}` };
71
- }
72
- const expiresAt = new Date(EPOCH.getTime() + daysSinceEpoch * 24 * 60 * 60 * 1000);
73
- if (new Date() > expiresAt) {
74
- return {
75
- isPro: false,
76
- expiresAt,
77
- customerId,
78
- reason: `License expired on ${expiresAt.toISOString().slice(0, 10)}`,
79
- };
80
- }
81
- return { isPro: true, expiresAt, customerId, reason: "Valid Pro license" };
82
- }
83
- export function formatUpgradePrompt(toolName, featureDescription) {
84
- return [
85
- `## ${toolName} (Pro Feature)`,
86
- "",
87
- "This analysis is available with MCP Java Backend Suite Pro.",
88
- "",
89
- `**What you'll get:**`,
90
- featureDescription,
91
- "",
92
- "**Upgrade**: https://mcpjbs.dev/pricing",
93
- "**Price**: $19/month or $190/year",
94
- "",
95
- "> Already have a key? Set `MCP_LICENSE_KEY` in your Claude Desktop config.",
96
- ].join("\n");
97
- }
98
- function base32Decode(encoded) {
99
- const bytes = [];
100
- let bits = 0;
101
- let value = 0;
102
- for (const char of encoded) {
103
- const idx = BASE32_CHARS.indexOf(char);
104
- if (idx === -1)
105
- throw new Error(`Invalid base32 character: ${char}`);
106
- value = (value << 5) | idx;
107
- bits += 5;
108
- if (bits >= 8) {
109
- bits -= 8;
110
- bytes.push((value >> bits) & 0xff);
111
- }
112
- }
113
- return Buffer.from(bytes);
114
- }
115
- //# sourceMappingURL=license.js.map