@kryptosai/mcp-observatory 1.28.2 → 1.28.3
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/CONTRIBUTORS.md +4 -1
- package/README.md +109 -14
- package/README.zh-CN.md +654 -0
- package/dist/src/adapters/http.js +13 -4
- package/dist/src/adapters/http.js.map +1 -1
- package/dist/src/checks/runtime-profile.d.ts +5 -0
- package/dist/src/checks/runtime-profile.js +214 -0
- package/dist/src/checks/runtime-profile.js.map +1 -0
- package/dist/src/checks/security-rules.js +18 -0
- package/dist/src/checks/security-rules.js.map +1 -1
- package/dist/src/checks/security.js +33 -65
- package/dist/src/checks/security.js.map +1 -1
- package/dist/src/checks/skill-scan.d.ts +46 -0
- package/dist/src/checks/skill-scan.js +665 -0
- package/dist/src/checks/skill-scan.js.map +1 -0
- package/dist/src/cli.js +15 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/enforce.d.ts +12 -0
- package/dist/src/commands/enforce.js +187 -0
- package/dist/src/commands/enforce.js.map +1 -0
- package/dist/src/commands/helpers.d.ts +2 -1
- package/dist/src/commands/helpers.js +9 -1
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/legacy.js +3 -0
- package/dist/src/commands/legacy.js.map +1 -1
- package/dist/src/commands/risk-graph.d.ts +2 -0
- package/dist/src/commands/risk-graph.js +64 -0
- package/dist/src/commands/risk-graph.js.map +1 -0
- package/dist/src/commands/scan.js +36 -9
- package/dist/src/commands/scan.js.map +1 -1
- package/dist/src/commands/score.js.map +1 -1
- package/dist/src/commands/skill-scan.d.ts +6 -0
- package/dist/src/commands/skill-scan.js +62 -0
- package/dist/src/commands/skill-scan.js.map +1 -0
- package/dist/src/commands/test.js +13 -0
- package/dist/src/commands/test.js.map +1 -1
- package/dist/src/commands/watch.js +13 -8
- package/dist/src/commands/watch.js.map +1 -1
- package/dist/src/discovery.d.ts +15 -1
- package/dist/src/discovery.js +251 -35
- package/dist/src/discovery.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/receipt.d.ts +4 -1
- package/dist/src/receipt.js +85 -0
- package/dist/src/receipt.js.map +1 -1
- package/dist/src/reporters/markdown.js +39 -0
- package/dist/src/reporters/markdown.js.map +1 -1
- package/dist/src/reporters/terminal.d.ts +2 -0
- package/dist/src/reporters/terminal.js +55 -2
- package/dist/src/reporters/terminal.js.map +1 -1
- package/dist/src/risk-graph.d.ts +64 -0
- package/dist/src/risk-graph.js +452 -0
- package/dist/src/risk-graph.js.map +1 -0
- package/dist/src/runner.js +6 -0
- package/dist/src/runner.js.map +1 -1
- package/dist/src/server.d.ts +1 -3
- package/dist/src/server.js +29 -645
- package/dist/src/server.js.map +1 -1
- package/dist/src/storage.js +13 -9
- package/dist/src/storage.js.map +1 -1
- package/dist/src/telemetry.d.ts +7 -0
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/tools/check-server.d.ts +27 -0
- package/dist/src/tools/check-server.js +46 -0
- package/dist/src/tools/check-server.js.map +1 -0
- package/dist/src/tools/ci-report.d.ts +21 -0
- package/dist/src/tools/ci-report.js +38 -0
- package/dist/src/tools/ci-report.js.map +1 -0
- package/dist/src/tools/diff-runs.d.ts +28 -0
- package/dist/src/tools/diff-runs.js +39 -0
- package/dist/src/tools/diff-runs.js.map +1 -0
- package/dist/src/tools/get-history.d.ts +21 -0
- package/dist/src/tools/get-history.js +37 -0
- package/dist/src/tools/get-history.js.map +1 -0
- package/dist/src/tools/get-last-run.d.ts +21 -0
- package/dist/src/tools/get-last-run.js +47 -0
- package/dist/src/tools/get-last-run.js.map +1 -0
- package/dist/src/tools/helpers.d.ts +8 -0
- package/dist/src/tools/helpers.js +32 -0
- package/dist/src/tools/helpers.js.map +1 -0
- package/dist/src/tools/lock-verify.d.ts +21 -0
- package/dist/src/tools/lock-verify.js +47 -0
- package/dist/src/tools/lock-verify.js.map +1 -0
- package/dist/src/tools/record.d.ts +23 -0
- package/dist/src/tools/record.js +48 -0
- package/dist/src/tools/record.js.map +1 -0
- package/dist/src/tools/replay.d.ts +21 -0
- package/dist/src/tools/replay.js +54 -0
- package/dist/src/tools/replay.js.map +1 -0
- package/dist/src/tools/scan.d.ts +18 -0
- package/dist/src/tools/scan.js +43 -0
- package/dist/src/tools/scan.js.map +1 -0
- package/dist/src/tools/score-server.d.ts +23 -0
- package/dist/src/tools/score-server.js +53 -0
- package/dist/src/tools/score-server.js.map +1 -0
- package/dist/src/tools/suggest-servers.d.ts +21 -0
- package/dist/src/tools/suggest-servers.js +114 -0
- package/dist/src/tools/suggest-servers.js.map +1 -0
- package/dist/src/tools/verify-tool.d.ts +25 -0
- package/dist/src/tools/verify-tool.js +48 -0
- package/dist/src/tools/verify-tool.js.map +1 -0
- package/dist/src/tools/watch.d.ts +23 -0
- package/dist/src/tools/watch.js +67 -0
- package/dist/src/tools/watch.js.map +1 -0
- package/dist/src/types.d.ts +26 -1
- package/dist/src/utils/security.d.ts +3 -0
- package/dist/src/utils/security.js +37 -0
- package/dist/src/utils/security.js.map +1 -0
- package/dist/src/validate.d.ts +1 -0
- package/dist/src/validate.js +119 -35
- package/dist/src/validate.js.map +1 -1
- package/docs/agent-tasks.md +1 -1
- package/docs/commercial-boundary.md +9 -1
- package/docs/contributor-recognition.md +2 -0
- package/docs/demo.svg +1 -59
- package/docs/mcp-receipts.md +9 -0
- package/docs/mcp-server-safety-index.md +21 -16
- package/docs/metrics-dashboard.md +17 -1
- package/docs/private-mcp-fleet-risk-graph.md +69 -0
- package/docs/receipt-graph.md +118 -31
- package/docs/safety-index/artifacts/chrome-devtools-mcp-server.json +1745 -0
- package/docs/safety-index/artifacts/chrome-devtools-mcp-server.md +169 -0
- package/docs/safety-index/artifacts/filesystem-mcp-server.json +786 -0
- package/docs/safety-index/artifacts/filesystem-mcp-server.md +169 -0
- package/docs/safety-index/artifacts/github-mcp-server.json +1707 -0
- package/docs/safety-index/artifacts/github-mcp-server.md +169 -0
- package/docs/safety-index/mcp-risk-graph.html +167 -0
- package/docs/safety-index/mcp-risk-graph.json +669 -0
- package/docs/safety-index/mcp-risk-graph.md +47 -0
- package/docs/safety-index/targets.json +57 -0
- package/docs/sample-private-fleet-risk-graph.md +90 -0
- package/package.json +21 -11
package/dist/src/server.js
CHANGED
|
@@ -1,657 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readdir } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { defaultCassettesDirectory, loadCassette, saveCassette } from "./cassette.js";
|
|
8
|
-
import { diffArtifacts } from "./diff.js";
|
|
9
|
-
import { scanForTargets } from "./discovery.js";
|
|
10
|
-
import { detectEnvironment } from "./environment.js";
|
|
11
|
-
import { renderMarkdown } from "./reporters/markdown.js";
|
|
12
|
-
import { errorMessage } from "./utils/errors.js";
|
|
13
|
-
import { runTarget, runTargetRecording } from "./runner.js";
|
|
14
|
-
import { defaultRunsDirectory, readArtifact, writeRunArtifact } from "./storage.js";
|
|
15
|
-
import { compareResponses } from "./verify.js";
|
|
16
|
-
import { loadTelemetryConfig, recordEvent, buildEvent } from "./telemetry.js";
|
|
17
4
|
import { TOOL_VERSION } from "./version.js";
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"bun",
|
|
34
|
-
]);
|
|
35
|
-
// ── Security: Argument Validation ──────────────────────────────────────────
|
|
36
|
-
// Reject args containing shell metacharacters that could enable injection.
|
|
37
|
-
const DANGEROUS_ARG_PATTERN = /[;|`]|\$\(|&&|\|\|/;
|
|
38
|
-
export function validateArgs(args) {
|
|
39
|
-
for (const arg of args) {
|
|
40
|
-
if (DANGEROUS_ARG_PATTERN.test(arg)) {
|
|
41
|
-
throw new Error(`Argument "${arg}" contains shell metacharacters and was rejected. ` +
|
|
42
|
-
`Remove characters like ; | && || $() or backticks.`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export function validateCommand(command) {
|
|
47
|
-
const base = path.basename(command.split(/\s+/)[0] ?? "");
|
|
48
|
-
if (!ALLOWED_COMMANDS.has(base)) {
|
|
49
|
-
throw new Error(`Command "${base}" is not in the MCP server allowlist. ` +
|
|
50
|
-
`Allowed executables: ${[...ALLOWED_COMMANDS].join(", ")}. ` +
|
|
51
|
-
`Use the CLI for arbitrary commands.`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// ── Security: Path Validation ──────────────────────────────────────────────
|
|
55
|
-
// File-reading tools must not traverse outside expected directories.
|
|
56
|
-
export function validatePath(filePath, allowedRoot) {
|
|
57
|
-
const resolved = path.resolve(filePath);
|
|
58
|
-
const root = path.resolve(allowedRoot);
|
|
59
|
-
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
60
|
-
throw new Error(`Path "${filePath}" resolves outside allowed directory "${allowedRoot}".`);
|
|
61
|
-
}
|
|
62
|
-
return resolved;
|
|
63
|
-
}
|
|
64
|
-
// ── Observability ──────────────────────────────────────────────────────────
|
|
65
|
-
function logRequest(tool, startMs, error, enrichment) {
|
|
66
|
-
const durationMs = Date.now() - startMs;
|
|
67
|
-
const status = error ? "ERROR" : "OK";
|
|
68
|
-
process.stderr.write(`[observatory] ${tool} ${status} ${durationMs}ms\n`);
|
|
69
|
-
recordEvent(buildEvent("tool_call", tool, "mcp", { executionMs: durationMs, ...enrichment }));
|
|
70
|
-
}
|
|
71
|
-
function formatRun(artifact) {
|
|
72
|
-
const lines = [];
|
|
73
|
-
lines.push(`Target: ${artifact.target.targetId}`);
|
|
74
|
-
lines.push(`Gate: ${artifact.gate}`);
|
|
75
|
-
lines.push(`Created: ${artifact.createdAt}`);
|
|
76
|
-
if (artifact.target.serverName) {
|
|
77
|
-
lines.push(`Server: ${artifact.target.serverName} ${artifact.target.serverVersion ?? ""}`);
|
|
78
|
-
}
|
|
79
|
-
lines.push("");
|
|
80
|
-
for (const check of artifact.checks) {
|
|
81
|
-
lines.push(` [${check.status}] ${check.id}: ${check.message}`);
|
|
82
|
-
}
|
|
83
|
-
if (artifact.fatalError) {
|
|
84
|
-
lines.push(`\nFatal error: ${artifact.fatalError}`);
|
|
85
|
-
}
|
|
86
|
-
return lines.join("\n");
|
|
87
|
-
}
|
|
5
|
+
import { ensureTelemetryWarm } from "./tools/helpers.js";
|
|
6
|
+
import * as scanTool from "./tools/scan.js";
|
|
7
|
+
import * as checkServerTool from "./tools/check-server.js";
|
|
8
|
+
import * as scoreServerTool from "./tools/score-server.js";
|
|
9
|
+
import * as diffRunsTool from "./tools/diff-runs.js";
|
|
10
|
+
import * as getLastRunTool from "./tools/get-last-run.js";
|
|
11
|
+
import * as suggestServersTool from "./tools/suggest-servers.js";
|
|
12
|
+
import * as recordTool from "./tools/record.js";
|
|
13
|
+
import * as replayTool from "./tools/replay.js";
|
|
14
|
+
import * as verifyTool from "./tools/verify-tool.js";
|
|
15
|
+
import * as watchTool from "./tools/watch.js";
|
|
16
|
+
import * as lockVerifyTool from "./tools/lock-verify.js";
|
|
17
|
+
import * as getHistoryTool from "./tools/get-history.js";
|
|
18
|
+
import * as ciReportTool from "./tools/ci-report.js";
|
|
19
|
+
export { validateArgs, validateCommand, validatePath } from "./utils/security.js";
|
|
88
20
|
export async function startServer() {
|
|
89
|
-
|
|
90
|
-
await loadTelemetryConfig();
|
|
21
|
+
await ensureTelemetryWarm();
|
|
91
22
|
const server = new McpServer({
|
|
92
23
|
name: "mcp-observatory",
|
|
93
24
|
version: TOOL_VERSION,
|
|
94
25
|
});
|
|
95
|
-
server.tool(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
opts.invokeTools = true;
|
|
109
|
-
if (security)
|
|
110
|
-
opts.securityCheck = true;
|
|
111
|
-
const lines = [`Discovered ${targets.length} server(s):\n`];
|
|
112
|
-
for (const t of targets) {
|
|
113
|
-
if (t.config.targetId === "mcp-observatory")
|
|
114
|
-
continue;
|
|
115
|
-
lines.push(`--- ${t.config.targetId} (from ${t.source}) ---`);
|
|
116
|
-
try {
|
|
117
|
-
const artifact = await runTarget(t.config, opts);
|
|
118
|
-
lines.push(formatRun(artifact));
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
const msg = errorMessage(error);
|
|
122
|
-
lines.push(` Error: ${msg}`);
|
|
123
|
-
}
|
|
124
|
-
lines.push("");
|
|
125
|
-
}
|
|
126
|
-
logRequest("scan", startMs);
|
|
127
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
128
|
-
});
|
|
129
|
-
server.tool("check_server", "Use this to test a specific MCP server before installing or after updating it. Launches the server by command, checks all capabilities, and saves a run artifact for future comparison. Example: check_server({ command: 'npx -y @modelcontextprotocol/server-everything' }). Use deep=true to invoke tools, security=true to analyze schemas for vulnerabilities.", {
|
|
130
|
-
command: z.string().describe("The command to launch the MCP server (e.g. 'npx -y @modelcontextprotocol/server-everything')."),
|
|
131
|
-
args: z.array(z.string()).optional().describe("Additional arguments for the command."),
|
|
132
|
-
deep: z.boolean().optional().describe("Also invoke safe tools to verify they execute."),
|
|
133
|
-
security: z.boolean().optional().describe("Run security analysis on tool schemas."),
|
|
134
|
-
}, async ({ command, args, deep, security }) => {
|
|
135
|
-
const startMs = Date.now();
|
|
136
|
-
try {
|
|
137
|
-
validateCommand(command);
|
|
138
|
-
validateArgs(args ?? []);
|
|
139
|
-
const target = {
|
|
140
|
-
targetId: command,
|
|
141
|
-
adapter: "local-process",
|
|
142
|
-
command,
|
|
143
|
-
args: args ?? [],
|
|
144
|
-
timeoutMs: 15_000,
|
|
145
|
-
};
|
|
146
|
-
const opts = {};
|
|
147
|
-
if (deep)
|
|
148
|
-
opts.invokeTools = true;
|
|
149
|
-
if (security)
|
|
150
|
-
opts.securityCheck = true;
|
|
151
|
-
const artifact = await runTarget(target, opts);
|
|
152
|
-
const outDir = defaultRunsDirectory();
|
|
153
|
-
const outPath = await writeRunArtifact(artifact, outDir);
|
|
154
|
-
logRequest("check_server", startMs);
|
|
155
|
-
return {
|
|
156
|
-
content: [{ type: "text", text: `${formatRun(artifact)}\n\nArtifact saved: ${outPath}` }],
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
const msg = errorMessage(error);
|
|
161
|
-
logRequest("check_server", startMs, true);
|
|
162
|
-
return { content: [{ type: "text", text: `Error checking server: ${msg}` }], isError: true };
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
server.tool("score_server", "Use this to get a quick health grade for an MCP server. Runs all checks (capabilities, tool invocation, security) and returns a 0-100 score with A-F grade and detailed breakdown across protocol compliance, schema quality, security, reliability, and performance.", {
|
|
166
|
-
command: z.string().describe("The command to launch the MCP server."),
|
|
167
|
-
args: z.array(z.string()).optional().describe("Additional arguments for the command."),
|
|
168
|
-
}, async ({ command, args }) => {
|
|
169
|
-
const startMs = Date.now();
|
|
170
|
-
try {
|
|
171
|
-
validateCommand(command);
|
|
172
|
-
validateArgs(args ?? []);
|
|
173
|
-
const target = {
|
|
174
|
-
targetId: command,
|
|
175
|
-
adapter: "local-process",
|
|
176
|
-
command,
|
|
177
|
-
args: args ?? [],
|
|
178
|
-
timeoutMs: 15_000,
|
|
179
|
-
};
|
|
180
|
-
const artifact = await runTarget(target, { invokeTools: true, securityCheck: true });
|
|
181
|
-
const outDir = defaultRunsDirectory();
|
|
182
|
-
await writeRunArtifact(artifact, outDir);
|
|
183
|
-
const score = artifact.healthScore;
|
|
184
|
-
if (!score) {
|
|
185
|
-
logRequest("score_server", startMs);
|
|
186
|
-
return { content: [{ type: "text", text: `${formatRun(artifact)}\n\nCould not compute health score.` }] };
|
|
187
|
-
}
|
|
188
|
-
const lines = [
|
|
189
|
-
`MCP Health Score: ${score.overall}/100 (${score.grade})`,
|
|
190
|
-
"",
|
|
191
|
-
];
|
|
192
|
-
for (const dim of score.dimensions) {
|
|
193
|
-
lines.push(` ${dim.name}: ${dim.score}/100 (weight: ${Math.round(dim.weight * 100)}%)`);
|
|
194
|
-
for (const detail of dim.details) {
|
|
195
|
-
lines.push(` → ${detail}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
lines.push("", formatRun(artifact));
|
|
199
|
-
logRequest("score_server", startMs);
|
|
200
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
const msg = errorMessage(error);
|
|
204
|
-
logRequest("score_server", startMs, true);
|
|
205
|
-
return { content: [{ type: "text", text: `Error scoring server: ${msg}` }], isError: true };
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
server.tool("diff_runs", "Use this to find what changed between two server checks. Compares two run artifacts and surfaces regressions (things that broke), recoveries (things that got fixed), schema drift (added/removed/changed tool parameters), and gate status changes. Essential after updating a server.", {
|
|
209
|
-
base: z.string().describe("Path to the base run artifact JSON file."),
|
|
210
|
-
head: z.string().describe("Path to the head run artifact JSON file."),
|
|
211
|
-
format: z.enum(["markdown", "json"]).optional().describe("Output format: 'markdown' (default) or 'json'."),
|
|
212
|
-
}, async ({ base, head, format }) => {
|
|
213
|
-
const startMs = Date.now();
|
|
214
|
-
try {
|
|
215
|
-
const runsDir = defaultRunsDirectory();
|
|
216
|
-
const basePath = validatePath(base, runsDir);
|
|
217
|
-
const headPath = validatePath(head, runsDir);
|
|
218
|
-
const baseArtifact = await readArtifact(basePath);
|
|
219
|
-
const headArtifact = await readArtifact(headPath);
|
|
220
|
-
if (baseArtifact.artifactType !== "run" || headArtifact.artifactType !== "run") {
|
|
221
|
-
return { content: [{ type: "text", text: "Both files must be run artifacts (not diff artifacts)." }], isError: true };
|
|
222
|
-
}
|
|
223
|
-
const diff = diffArtifacts(baseArtifact, headArtifact);
|
|
224
|
-
const output = format === "json"
|
|
225
|
-
? JSON.stringify(diff, null, 2)
|
|
226
|
-
: renderMarkdown(diff);
|
|
227
|
-
logRequest("diff_runs", startMs);
|
|
228
|
-
return { content: [{ type: "text", text: output }] };
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
const msg = errorMessage(error);
|
|
232
|
-
logRequest("diff_runs", startMs, true);
|
|
233
|
-
return { content: [{ type: "text", text: `Error diffing runs: ${msg}` }], isError: true };
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
server.tool("get_last_run", "Use this to retrieve the last check results for a server. Finds the most recent run artifact by target ID so you can review previous results or diff against a new run.", {
|
|
237
|
-
targetId: z.string().describe("The target ID to find the last run for (e.g. server name or command)."),
|
|
238
|
-
}, async ({ targetId }) => {
|
|
239
|
-
const startMs = Date.now();
|
|
240
|
-
try {
|
|
241
|
-
const dir = defaultRunsDirectory();
|
|
242
|
-
let files;
|
|
243
|
-
try {
|
|
244
|
-
files = await readdir(dir);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return { content: [{ type: "text", text: `No runs directory found at ${dir}` }], isError: true };
|
|
248
|
-
}
|
|
249
|
-
const needle = targetId.toLowerCase();
|
|
250
|
-
const matching = files
|
|
251
|
-
.filter((f) => f.endsWith(".json") && f.toLowerCase().includes(needle))
|
|
252
|
-
.sort()
|
|
253
|
-
.reverse();
|
|
254
|
-
if (matching.length === 0) {
|
|
255
|
-
return { content: [{ type: "text", text: `No run artifacts found for target "${targetId}" in ${dir}` }] };
|
|
256
|
-
}
|
|
257
|
-
const latest = matching[0];
|
|
258
|
-
const artifact = await readArtifact(path.join(dir, latest));
|
|
259
|
-
if (artifact.artifactType !== "run") {
|
|
260
|
-
return { content: [{ type: "text", text: `Latest matching file is not a run artifact: ${latest}` }], isError: true };
|
|
261
|
-
}
|
|
262
|
-
logRequest("get_last_run", startMs);
|
|
263
|
-
return {
|
|
264
|
-
content: [{ type: "text", text: `${formatRun(artifact)}\n\nFile: ${path.join(dir, latest)}` }],
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
const msg = errorMessage(error);
|
|
269
|
-
logRequest("get_last_run", startMs, true);
|
|
270
|
-
return { content: [{ type: "text", text: `Error reading last run: ${msg}` }], isError: true };
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
server.tool("suggest_servers", "Use this when setting up a project or wondering what MCP servers to add. Scans the working directory for languages, frameworks, databases, and cloud providers, lists currently configured servers, and cross-references the MCP registry to recommend servers you're missing.", {
|
|
274
|
-
cwd: z.string().optional().describe("Working directory to scan for environment signals. Defaults to process.cwd()."),
|
|
275
|
-
}, async ({ cwd }) => {
|
|
276
|
-
const startMs = Date.now();
|
|
277
|
-
const workDir = cwd ?? process.cwd();
|
|
278
|
-
// Constrain to cwd subtree
|
|
279
|
-
const resolvedDir = path.resolve(workDir);
|
|
280
|
-
const cwdRoot = path.resolve(process.cwd());
|
|
281
|
-
if (!resolvedDir.startsWith(cwdRoot + path.sep) && resolvedDir !== cwdRoot) {
|
|
282
|
-
logRequest("suggest_servers", startMs, true);
|
|
283
|
-
return { content: [{ type: "text", text: `Path "${cwd}" resolves outside the working directory.` }], isError: true };
|
|
284
|
-
}
|
|
285
|
-
const sections = [];
|
|
286
|
-
// 1. Current MCP servers
|
|
287
|
-
try {
|
|
288
|
-
const targets = await scanForTargets();
|
|
289
|
-
if (targets.length > 0) {
|
|
290
|
-
const serverLines = targets.map((t) => {
|
|
291
|
-
const id = t.config.targetId;
|
|
292
|
-
const detail = t.config.adapter === "http"
|
|
293
|
-
? `(HTTP: ${t.config.url})`
|
|
294
|
-
: `(command: ${t.config.command} ${t.config.args.join(" ")})`;
|
|
295
|
-
return ` - ${id} ${detail} [source: ${t.source}]`;
|
|
296
|
-
});
|
|
297
|
-
sections.push(`## Currently Configured MCP Servers\n${serverLines.join("\n")}`);
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
sections.push("## Currently Configured MCP Servers\nNone found.");
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
catch (error) {
|
|
304
|
-
const msg = errorMessage(error);
|
|
305
|
-
sections.push(`## Currently Configured MCP Servers\nError scanning: ${msg}`);
|
|
306
|
-
}
|
|
307
|
-
// 2. Environment detection
|
|
308
|
-
try {
|
|
309
|
-
const env = await detectEnvironment(workDir);
|
|
310
|
-
const envLines = [];
|
|
311
|
-
if (env.languages.length > 0)
|
|
312
|
-
envLines.push(` Languages: ${env.languages.join(", ")}`);
|
|
313
|
-
if (env.frameworks.length > 0)
|
|
314
|
-
envLines.push(` Frameworks: ${env.frameworks.join(", ")}`);
|
|
315
|
-
if (env.databases.length > 0)
|
|
316
|
-
envLines.push(` Databases: ${env.databases.join(", ")}`);
|
|
317
|
-
if (env.cloud.length > 0)
|
|
318
|
-
envLines.push(` Cloud: ${env.cloud.join(", ")}`);
|
|
319
|
-
if (env.cicd.length > 0)
|
|
320
|
-
envLines.push(` CI/CD: ${env.cicd.join(", ")}`);
|
|
321
|
-
if (env.services.length > 0)
|
|
322
|
-
envLines.push(` Services (detected from .env keys): ${env.services.join(", ")}`);
|
|
323
|
-
if (envLines.length > 0) {
|
|
324
|
-
sections.push(`## Detected Environment (${workDir})\n${envLines.join("\n")}`);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
sections.push(`## Detected Environment (${workDir})\nNo recognizable project signals found.`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
const msg = errorMessage(error);
|
|
332
|
-
sections.push(`## Detected Environment\nError: ${msg}`);
|
|
333
|
-
}
|
|
334
|
-
// 3. MCP Registry
|
|
335
|
-
try {
|
|
336
|
-
const registryUrl = "https://registry.modelcontextprotocol.io/v0/servers";
|
|
337
|
-
const response = await fetch(registryUrl, {
|
|
338
|
-
signal: AbortSignal.timeout(10_000),
|
|
339
|
-
headers: { "Accept": "application/json" },
|
|
340
|
-
});
|
|
341
|
-
if (response.ok) {
|
|
342
|
-
const data = await response.json();
|
|
343
|
-
function formatRegistryEntry(entry) {
|
|
344
|
-
// Registry returns { server: { name, description, ... }, ... }
|
|
345
|
-
const srv = (typeof entry["server"] === "object" && entry["server"] !== null
|
|
346
|
-
? entry["server"] : entry);
|
|
347
|
-
const rawName = srv["name"] ?? entry["name"] ?? entry["id"];
|
|
348
|
-
const name = typeof rawName === "string" ? rawName : "unknown";
|
|
349
|
-
const rawDesc = srv["description"] ?? entry["description"];
|
|
350
|
-
const desc = typeof rawDesc === "string" ? ` — ${rawDesc}` : "";
|
|
351
|
-
return ` - ${name}${desc}`;
|
|
352
|
-
}
|
|
353
|
-
if (Array.isArray(data)) {
|
|
354
|
-
const entries = data.slice(0, 50);
|
|
355
|
-
const registryLines = entries.map(formatRegistryEntry);
|
|
356
|
-
sections.push(`## MCP Registry (${String(entries.length)} of ${String(data.length)} servers shown)\n${registryLines.join("\n")}`);
|
|
357
|
-
}
|
|
358
|
-
else if (typeof data === "object" && data !== null) {
|
|
359
|
-
const obj = data;
|
|
360
|
-
const servers = obj["servers"] ?? obj["results"] ?? obj["items"];
|
|
361
|
-
if (Array.isArray(servers)) {
|
|
362
|
-
const entries = servers.slice(0, 50);
|
|
363
|
-
const registryLines = entries.map(formatRegistryEntry);
|
|
364
|
-
sections.push(`## MCP Registry (${String(entries.length)} of ${String(servers.length)} servers shown)\n${registryLines.join("\n")}`);
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
sections.push(`## MCP Registry\nRegistry returned data but in an unexpected format. Keys: ${Object.keys(obj).join(", ")}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
sections.push(`## MCP Registry\nRegistry returned HTTP ${String(response.status)}. The listing endpoint may not be publicly available.`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
const msg = errorMessage(error);
|
|
377
|
-
sections.push(`## MCP Registry\nCould not reach registry: ${msg}`);
|
|
378
|
-
}
|
|
379
|
-
logRequest("suggest_servers", startMs);
|
|
380
|
-
return { content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
381
|
-
});
|
|
382
|
-
server.tool("record", "Use this to capture a baseline of a working MCP server. Records all JSON-RPC traffic to a cassette file that can be replayed offline (no server needed) or used to verify future versions haven't broken anything. Like VCR for MCP.", {
|
|
383
|
-
command: z.string().describe("The command to launch the MCP server."),
|
|
384
|
-
args: z.array(z.string()).optional().describe("Additional arguments for the command."),
|
|
385
|
-
}, async ({ command, args }) => {
|
|
386
|
-
const startMs = Date.now();
|
|
387
|
-
try {
|
|
388
|
-
validateCommand(command);
|
|
389
|
-
validateArgs(args ?? []);
|
|
390
|
-
const target = {
|
|
391
|
-
targetId: command,
|
|
392
|
-
adapter: "local-process",
|
|
393
|
-
command,
|
|
394
|
-
args: args ?? [],
|
|
395
|
-
timeoutMs: 15_000,
|
|
396
|
-
};
|
|
397
|
-
const { artifact, cassetteEntries } = await runTargetRecording(target, { invokeTools: true });
|
|
398
|
-
if (!cassetteEntries || cassetteEntries.length === 0) {
|
|
399
|
-
return { content: [{ type: "text", text: "No traffic recorded." }], isError: true };
|
|
400
|
-
}
|
|
401
|
-
const cassette = {
|
|
402
|
-
version: 1,
|
|
403
|
-
targetId: target.targetId,
|
|
404
|
-
recordedAt: new Date().toISOString(),
|
|
405
|
-
transport: "stdio",
|
|
406
|
-
entries: cassetteEntries,
|
|
407
|
-
};
|
|
408
|
-
const cassettePath = await saveCassette(cassette, defaultCassettesDirectory());
|
|
409
|
-
logRequest("record", startMs);
|
|
410
|
-
return {
|
|
411
|
-
content: [{ type: "text", text: `${formatRun(artifact)}\n\nCassette saved: ${cassettePath}\n${cassetteEntries.length} entries recorded.\n\nReplay offline: replay({ cassette: "${cassettePath}" })\nVerify live: verify({ cassette: "${cassettePath}", command: "${command}" })` }],
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
const msg = errorMessage(error);
|
|
416
|
-
logRequest("record", startMs, true);
|
|
417
|
-
return { content: [{ type: "text", text: `Error recording: ${msg}` }], isError: true };
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
server.tool("replay", "Use this to test a server without running it. Replays a previously recorded cassette offline and runs all checks against the recorded responses. Useful in CI or when the live server is unavailable.", {
|
|
421
|
-
cassette: z.string().describe("Path to a cassette JSON file."),
|
|
422
|
-
}, async ({ cassette: cassettePath }) => {
|
|
423
|
-
const startMs = Date.now();
|
|
424
|
-
try {
|
|
425
|
-
const cassettesDir = defaultCassettesDirectory();
|
|
426
|
-
validatePath(cassettePath, cassettesDir);
|
|
427
|
-
const { ReplayTransport } = await import("./transport/replay-transport.js");
|
|
428
|
-
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
429
|
-
const { runToolsCheck } = await import("./checks/tools.js");
|
|
430
|
-
const { runPromptsCheck } = await import("./checks/prompts.js");
|
|
431
|
-
const { runResourcesCheck } = await import("./checks/resources.js");
|
|
432
|
-
const { runToolsInvokeCheck } = await import("./checks/tools-invoke.js");
|
|
433
|
-
const cassette = await loadCassette(cassettePath);
|
|
434
|
-
const replayTransport = new ReplayTransport(cassette.entries);
|
|
435
|
-
const client = new Client({ name: "mcp-observatory", version: TOOL_VERSION }, { capabilities: {} });
|
|
436
|
-
await client.connect(replayTransport);
|
|
437
|
-
const checkContext = {
|
|
438
|
-
client,
|
|
439
|
-
serverCapabilities: client.getServerCapabilities(),
|
|
440
|
-
target: { targetId: cassette.targetId, adapter: "local-process", command: "replay", args: [] },
|
|
441
|
-
timeoutMs: 10_000,
|
|
442
|
-
stderrLines: [],
|
|
443
|
-
};
|
|
444
|
-
const checks = [
|
|
445
|
-
(await runToolsCheck(checkContext)).result,
|
|
446
|
-
(await runPromptsCheck(checkContext)).result,
|
|
447
|
-
(await runResourcesCheck(checkContext)).result,
|
|
448
|
-
(await runToolsInvokeCheck(checkContext)).result,
|
|
449
|
-
];
|
|
450
|
-
await client.close();
|
|
451
|
-
const lines = [`Replay of ${cassette.targetId} (${cassette.entries.length} entries):\n`];
|
|
452
|
-
for (const check of checks) {
|
|
453
|
-
lines.push(` [${check.status}] ${check.id}: ${check.message}`);
|
|
454
|
-
}
|
|
455
|
-
logRequest("replay", startMs);
|
|
456
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
457
|
-
}
|
|
458
|
-
catch (error) {
|
|
459
|
-
const msg = errorMessage(error);
|
|
460
|
-
logRequest("replay", startMs, true);
|
|
461
|
-
return { content: [{ type: "text", text: `Error replaying: ${msg}` }], isError: true };
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
server.tool("verify", "Use this after updating a server to confirm nothing broke. Connects to the live server, sends the same requests from a recorded cassette, and compares responses. Reports exactly what changed — added tools, removed parameters, different response shapes.", {
|
|
465
|
-
cassette: z.string().describe("Path to a cassette JSON file."),
|
|
466
|
-
command: z.string().describe("The command to launch the MCP server."),
|
|
467
|
-
args: z.array(z.string()).optional().describe("Additional arguments for the command."),
|
|
468
|
-
}, async ({ cassette: cassettePath, command, args }) => {
|
|
469
|
-
const startMs = Date.now();
|
|
470
|
-
try {
|
|
471
|
-
validateCommand(command);
|
|
472
|
-
const cassettesDir = defaultCassettesDirectory();
|
|
473
|
-
validatePath(cassettePath, cassettesDir);
|
|
474
|
-
const cassette = await loadCassette(cassettePath);
|
|
475
|
-
const target = {
|
|
476
|
-
targetId: command,
|
|
477
|
-
adapter: "local-process",
|
|
478
|
-
command,
|
|
479
|
-
args: args ?? [],
|
|
480
|
-
timeoutMs: 15_000,
|
|
481
|
-
};
|
|
482
|
-
const { cassetteEntries } = await runTargetRecording(target, { invokeTools: true });
|
|
483
|
-
if (!cassetteEntries) {
|
|
484
|
-
return { content: [{ type: "text", text: "Failed to record live session for comparison." }], isError: true };
|
|
485
|
-
}
|
|
486
|
-
const result = compareResponses(cassette, cassetteEntries);
|
|
487
|
-
const lines = [`Verify: ${result.passed} passed, ${result.failed} changed, ${result.missing} missing\n`];
|
|
488
|
-
for (const entry of result.entries) {
|
|
489
|
-
const icon = entry.status === "pass" ? "✓" : entry.status === "fail" ? "✗" : "?";
|
|
490
|
-
lines.push(` ${icon} ${entry.method}${entry.diff ? ` — ${entry.diff}` : ""}`);
|
|
491
|
-
}
|
|
492
|
-
logRequest("verify", startMs);
|
|
493
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
const msg = errorMessage(error);
|
|
497
|
-
logRequest("verify", startMs, true);
|
|
498
|
-
return { content: [{ type: "text", text: `Error verifying: ${msg}` }], isError: true };
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
server.tool("watch", "Use this to check a server and see what changed since the last check. Runs all checks, saves the result, and diffs against the previous run for the same target. Shows regressions, recoveries, and schema drift in one call.", {
|
|
502
|
-
command: z.string().describe("The command to launch the MCP server."),
|
|
503
|
-
args: z.array(z.string()).optional().describe("Additional arguments for the command."),
|
|
504
|
-
}, async ({ command, args }) => {
|
|
505
|
-
const startMs = Date.now();
|
|
506
|
-
try {
|
|
507
|
-
validateCommand(command);
|
|
508
|
-
validateArgs(args ?? []);
|
|
509
|
-
const target = {
|
|
510
|
-
targetId: command,
|
|
511
|
-
adapter: "local-process",
|
|
512
|
-
command,
|
|
513
|
-
args: args ?? [],
|
|
514
|
-
timeoutMs: 15_000,
|
|
515
|
-
};
|
|
516
|
-
// Run current check
|
|
517
|
-
const artifact = await runTarget(target);
|
|
518
|
-
const outDir = defaultRunsDirectory();
|
|
519
|
-
const outPath = await writeRunArtifact(artifact, outDir);
|
|
520
|
-
// Find previous run to diff against
|
|
521
|
-
let diffText = "";
|
|
522
|
-
try {
|
|
523
|
-
const files = await readdir(outDir);
|
|
524
|
-
const needle = target.targetId.toLowerCase();
|
|
525
|
-
const matching = files
|
|
526
|
-
.filter((f) => f.endsWith(".json") && f.toLowerCase().includes(needle) && path.join(outDir, f) !== outPath)
|
|
527
|
-
.sort()
|
|
528
|
-
.reverse();
|
|
529
|
-
if (matching.length > 0) {
|
|
530
|
-
const prevArtifact = await readArtifact(path.join(outDir, matching[0]));
|
|
531
|
-
if (prevArtifact.artifactType === "run") {
|
|
532
|
-
const diff = diffArtifacts(prevArtifact, artifact);
|
|
533
|
-
if (diff.summary.regressions > 0 || diff.summary.recoveries > 0 || diff.summary.added > 0 || diff.summary.removed > 0) {
|
|
534
|
-
diffText = `\n\nChanges since last run:\n${renderMarkdown(diff)}`;
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
diffText = "\n\nNo changes since last run.";
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
catch {
|
|
543
|
-
// No previous run — that's fine
|
|
544
|
-
}
|
|
545
|
-
logRequest("watch", startMs);
|
|
546
|
-
return {
|
|
547
|
-
content: [{ type: "text", text: `${formatRun(artifact)}\n\nArtifact saved: ${outPath}${diffText}` }],
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
const msg = errorMessage(error);
|
|
552
|
-
logRequest("watch", startMs, true);
|
|
553
|
-
return { content: [{ type: "text", text: `Error watching: ${msg}` }], isError: true };
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
server.tool("lock_verify", "Verify that live MCP servers still match a previously saved lock file. Detects schema drift, added/removed tools, and breaking changes.", {
|
|
557
|
-
config: z.string().optional().describe("Path to MCP config file."),
|
|
558
|
-
}, async ({ config }) => {
|
|
559
|
-
const startMs = Date.now();
|
|
560
|
-
try {
|
|
561
|
-
const lockFile = await readLockFile();
|
|
562
|
-
const targets = await scanForTargets(config);
|
|
563
|
-
const results = [];
|
|
564
|
-
let anyFailed = false;
|
|
565
|
-
for (const t of targets) {
|
|
566
|
-
const lockEntry = lockFile.servers.find(s => s.targetId === t.config.targetId);
|
|
567
|
-
if (!lockEntry)
|
|
568
|
-
continue;
|
|
569
|
-
const artifact = await runTarget(t.config);
|
|
570
|
-
const result = verifyAgainstLock(lockEntry, artifact);
|
|
571
|
-
if (result.passed) {
|
|
572
|
-
results.push(`✓ ${t.config.targetId}: no drift`);
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
anyFailed = true;
|
|
576
|
-
results.push(`✗ ${t.config.targetId}: ${result.drift.length} changes`);
|
|
577
|
-
for (const d of result.drift) {
|
|
578
|
-
results.push(` - ${d.category}: ${d.name} — ${d.change}`);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
if (results.length === 0) {
|
|
583
|
-
results.push("No servers in lock file match discovered targets.");
|
|
584
|
-
}
|
|
585
|
-
logRequest("lock_verify", startMs, anyFailed);
|
|
586
|
-
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
587
|
-
}
|
|
588
|
-
catch (error) {
|
|
589
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
590
|
-
logRequest("lock_verify", startMs, true);
|
|
591
|
-
return { content: [{ type: "text", text: `Lock verify failed: ${msg}` }], isError: true };
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
server.tool("get_history", "Get health score trends for MCP servers from run history.", {
|
|
595
|
-
target: z.string().optional().describe("Filter to a specific target ID."),
|
|
596
|
-
}, async ({ target }) => {
|
|
597
|
-
const startMs = Date.now();
|
|
598
|
-
try {
|
|
599
|
-
const history = await readHistory();
|
|
600
|
-
let targetIds = [...new Set(history.entries.map(e => e.targetId))];
|
|
601
|
-
if (target)
|
|
602
|
-
targetIds = targetIds.filter(id => id === target);
|
|
603
|
-
if (targetIds.length === 0) {
|
|
604
|
-
logRequest("get_history", startMs);
|
|
605
|
-
return { content: [{ type: "text", text: "No history found. Run a scan or test first." }] };
|
|
606
|
-
}
|
|
607
|
-
const lines = [];
|
|
608
|
-
for (const id of targetIds) {
|
|
609
|
-
const trend = getTrend(id, history);
|
|
610
|
-
if (!trend)
|
|
611
|
-
continue;
|
|
612
|
-
const { current } = trend;
|
|
613
|
-
const label = renderTrendLabel(trend);
|
|
614
|
-
lines.push(`${id}: ${current.grade} (${current.healthScore}) ${label}`);
|
|
615
|
-
}
|
|
616
|
-
logRequest("get_history", startMs);
|
|
617
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
618
|
-
}
|
|
619
|
-
catch (error) {
|
|
620
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
621
|
-
logRequest("get_history", startMs, true);
|
|
622
|
-
return { content: [{ type: "text", text: `History failed: ${msg}` }], isError: true };
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
server.tool("ci_report", "Generate a CI regression report from run artifacts.", {
|
|
626
|
-
artifactsDir: z.string().optional().describe("Directory containing run artifacts. Defaults to .mcp-observatory/runs/"),
|
|
627
|
-
}, async ({ artifactsDir }) => {
|
|
628
|
-
const startMs = Date.now();
|
|
629
|
-
try {
|
|
630
|
-
const { readdir, readFile } = await import("node:fs/promises");
|
|
631
|
-
const dir = artifactsDir ?? path.join(process.cwd(), ".mcp-observatory", "runs");
|
|
632
|
-
const files = await readdir(dir);
|
|
633
|
-
const artifacts = [];
|
|
634
|
-
for (const f of files) {
|
|
635
|
-
if (!f.endsWith(".json"))
|
|
636
|
-
continue;
|
|
637
|
-
try {
|
|
638
|
-
const raw = await readFile(path.join(dir, f), "utf8");
|
|
639
|
-
const parsed = JSON.parse(raw);
|
|
640
|
-
if (parsed["artifactType"] === "run")
|
|
641
|
-
artifacts.push(parsed);
|
|
642
|
-
}
|
|
643
|
-
catch { /* skip invalid */ }
|
|
644
|
-
}
|
|
645
|
-
const report = buildCiReport(artifacts);
|
|
646
|
-
logRequest("ci_report", startMs);
|
|
647
|
-
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
651
|
-
logRequest("ci_report", startMs, true);
|
|
652
|
-
return { content: [{ type: "text", text: `CI report failed: ${msg}` }], isError: true };
|
|
653
|
-
}
|
|
654
|
-
});
|
|
26
|
+
server.tool(scanTool.name, scanTool.description, scanTool.schema, scanTool.handler);
|
|
27
|
+
server.tool(checkServerTool.name, checkServerTool.description, checkServerTool.schema, checkServerTool.handler);
|
|
28
|
+
server.tool(scoreServerTool.name, scoreServerTool.description, scoreServerTool.schema, scoreServerTool.handler);
|
|
29
|
+
server.tool(diffRunsTool.name, diffRunsTool.description, diffRunsTool.schema, diffRunsTool.handler);
|
|
30
|
+
server.tool(getLastRunTool.name, getLastRunTool.description, getLastRunTool.schema, getLastRunTool.handler);
|
|
31
|
+
server.tool(suggestServersTool.name, suggestServersTool.description, suggestServersTool.schema, suggestServersTool.handler);
|
|
32
|
+
server.tool(recordTool.name, recordTool.description, recordTool.schema, recordTool.handler);
|
|
33
|
+
server.tool(replayTool.name, replayTool.description, replayTool.schema, replayTool.handler);
|
|
34
|
+
server.tool(verifyTool.name, verifyTool.description, verifyTool.schema, verifyTool.handler);
|
|
35
|
+
server.tool(watchTool.name, watchTool.description, watchTool.schema, watchTool.handler);
|
|
36
|
+
server.tool(lockVerifyTool.name, lockVerifyTool.description, lockVerifyTool.schema, lockVerifyTool.handler);
|
|
37
|
+
server.tool(getHistoryTool.name, getHistoryTool.description, getHistoryTool.schema, getHistoryTool.handler);
|
|
38
|
+
server.tool(ciReportTool.name, ciReportTool.description, ciReportTool.schema, ciReportTool.handler);
|
|
655
39
|
const transport = new StdioServerTransport();
|
|
656
40
|
await server.connect(transport);
|
|
657
41
|
}
|