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 +1 -1
- package/build/index.js +210 -183
- package/package.json +2 -1
- package/build/license.js +0 -115
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
output +=
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
for (const
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|