@kryptosai/mcp-observatory 0.14.0 → 0.14.1
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/dist/src/adapters/http.js +6 -2
- package/dist/src/adapters/http.js.map +1 -1
- package/dist/src/adapters/local-process.js +25 -3
- package/dist/src/adapters/local-process.js.map +1 -1
- package/dist/src/checks/conformance.js +5 -4
- package/dist/src/checks/conformance.js.map +1 -1
- package/dist/src/checks/list-check.js +2 -1
- package/dist/src/checks/list-check.js.map +1 -1
- package/dist/src/checks/resources.js +3 -2
- package/dist/src/checks/resources.js.map +1 -1
- package/dist/src/checks/tools-invoke.js +3 -2
- package/dist/src/checks/tools-invoke.js.map +1 -1
- package/dist/src/cli.js +36 -901
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/diff.d.ts +2 -0
- package/dist/src/commands/diff.js +27 -0
- package/dist/src/commands/diff.js.map +1 -0
- package/dist/src/commands/helpers.d.ts +25 -0
- package/dist/src/commands/helpers.js +131 -0
- package/dist/src/commands/helpers.js.map +1 -0
- package/dist/src/commands/legacy.d.ts +2 -0
- package/dist/src/commands/legacy.js +77 -0
- package/dist/src/commands/legacy.js.map +1 -0
- package/dist/src/commands/record-replay.d.ts +2 -0
- package/dist/src/commands/record-replay.js +181 -0
- package/dist/src/commands/record-replay.js.map +1 -0
- package/dist/src/commands/scan.d.ts +2 -0
- package/dist/src/commands/scan.js +155 -0
- package/dist/src/commands/scan.js.map +1 -0
- package/dist/src/commands/score.d.ts +2 -0
- package/dist/src/commands/score.js +88 -0
- package/dist/src/commands/score.js.map +1 -0
- package/dist/src/commands/serve.d.ts +2 -0
- package/dist/src/commands/serve.js +10 -0
- package/dist/src/commands/serve.js.map +1 -0
- package/dist/src/commands/suggest.d.ts +2 -0
- package/dist/src/commands/suggest.js +126 -0
- package/dist/src/commands/suggest.js.map +1 -0
- package/dist/src/commands/telemetry.d.ts +2 -0
- package/dist/src/commands/telemetry.js +65 -0
- package/dist/src/commands/telemetry.js.map +1 -0
- package/dist/src/commands/test.d.ts +2 -0
- package/dist/src/commands/test.js +37 -0
- package/dist/src/commands/test.js.map +1 -0
- package/dist/src/commands/watch.d.ts +5 -0
- package/dist/src/commands/watch.js +46 -0
- package/dist/src/commands/watch.js.map +1 -0
- package/dist/src/environment.js +12 -4
- package/dist/src/environment.js.map +1 -1
- package/dist/src/runner.js +2 -1
- package/dist/src/runner.js.map +1 -1
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +30 -14
- package/dist/src/server.js.map +1 -1
- package/dist/src/utils/errors.d.ts +4 -0
- package/dist/src/utils/errors.js +7 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { defaultCassettesDirectory, loadCassette, saveCassette } from "../cassette.js";
|
|
4
|
+
import { runPromptsCheck } from "../checks/prompts.js";
|
|
5
|
+
import { runResourcesCheck } from "../checks/resources.js";
|
|
6
|
+
import { runToolsCheck } from "../checks/tools.js";
|
|
7
|
+
import { runToolsInvokeCheck } from "../checks/tools-invoke.js";
|
|
8
|
+
import { renderTerminal, } from "../index.js";
|
|
9
|
+
import { runTargetRecording } from "../runner.js";
|
|
10
|
+
import { ReplayTransport } from "../transport/replay-transport.js";
|
|
11
|
+
import { SCHEMA_VERSION } from "../types.js";
|
|
12
|
+
import { buildRunId } from "../utils/ids.js";
|
|
13
|
+
import { compareResponses } from "../verify.js";
|
|
14
|
+
import { TOOL_VERSION } from "../version.js";
|
|
15
|
+
import { ANSI, c, readTargetConfig, targetFromCommand, getPassthroughArgs } from "./helpers.js";
|
|
16
|
+
export function registerRecordReplayCommands(program, bin) {
|
|
17
|
+
// ── record ─────────────────────────────────────────────────────────────
|
|
18
|
+
program
|
|
19
|
+
.command("record")
|
|
20
|
+
.passThroughOptions()
|
|
21
|
+
.description("Record a server session to a cassette file for replay.")
|
|
22
|
+
.argument("[command...]", "Server command and arguments to run.")
|
|
23
|
+
.option("--target <config>", "Path to a target config JSON file.")
|
|
24
|
+
.option("--no-color", "Disable colored output.")
|
|
25
|
+
.action(async (commandArgs, options) => {
|
|
26
|
+
const target = options.target
|
|
27
|
+
? await readTargetConfig(options.target)
|
|
28
|
+
: targetFromCommand(commandArgs.length > 0 ? commandArgs : getPassthroughArgs());
|
|
29
|
+
process.stdout.write(`${c(ANSI.dim, "⟳")} Recording session with ${c(ANSI.bold, target.targetId)}...\n`);
|
|
30
|
+
const { artifact, cassetteEntries } = await runTargetRecording(target, { invokeTools: true });
|
|
31
|
+
if (!cassetteEntries || cassetteEntries.length === 0) {
|
|
32
|
+
process.stdout.write(`${c(ANSI.yellow, "⚠")} No traffic recorded.\n`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const cassette = {
|
|
37
|
+
version: 1,
|
|
38
|
+
targetId: target.targetId,
|
|
39
|
+
recordedAt: new Date().toISOString(),
|
|
40
|
+
transport: target.adapter === "http" ? "http" : "stdio",
|
|
41
|
+
entries: cassetteEntries,
|
|
42
|
+
};
|
|
43
|
+
const cassettePath = await saveCassette(cassette, defaultCassettesDirectory(process.cwd()));
|
|
44
|
+
const summary = renderTerminal(artifact);
|
|
45
|
+
process.stdout.write(`\n${summary}\n`);
|
|
46
|
+
process.stdout.write(`\n${c(ANSI.green, "✓")} Cassette saved: ${cassettePath}\n`);
|
|
47
|
+
process.stdout.write(` ${c(ANSI.dim, `${cassetteEntries.length} entries recorded`)}\n`);
|
|
48
|
+
process.stdout.write(`\n Replay offline: ${c(ANSI.cyan, `${bin} replay ${cassettePath}`)}\n`);
|
|
49
|
+
process.stdout.write(` Verify live: ${c(ANSI.cyan, `${bin} verify ${cassettePath} ${target.adapter === "http" ? `--target <config>` : commandArgs.join(" ")}`)}\n\n`);
|
|
50
|
+
if (artifact.gate === "fail") {
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// ── replay ─────────────────────────────────────────────────────────────
|
|
55
|
+
program
|
|
56
|
+
.command("replay")
|
|
57
|
+
.description("Replay a cassette file offline — no live server needed.")
|
|
58
|
+
.argument("<cassette>", "Path to a cassette JSON file.")
|
|
59
|
+
.option("--no-color", "Disable colored output.")
|
|
60
|
+
.action(async (cassettePath) => {
|
|
61
|
+
const cassette = await loadCassette(cassettePath);
|
|
62
|
+
process.stdout.write(`${c(ANSI.dim, "⟳")} Replaying cassette for ${c(ANSI.bold, cassette.targetId)} (${cassette.entries.length} entries)...\n`);
|
|
63
|
+
// Create a target config for the replay
|
|
64
|
+
const replayTarget = {
|
|
65
|
+
targetId: cassette.targetId,
|
|
66
|
+
adapter: "local-process",
|
|
67
|
+
command: "replay",
|
|
68
|
+
args: [],
|
|
69
|
+
};
|
|
70
|
+
// Build a ReplayTransport and run checks against it
|
|
71
|
+
const transport = new ReplayTransport(cassette.entries);
|
|
72
|
+
const client = new Client({ name: "mcp-observatory", version: TOOL_VERSION }, { capabilities: {} });
|
|
73
|
+
await client.connect(transport);
|
|
74
|
+
const serverCapabilities = client.getServerCapabilities();
|
|
75
|
+
const checkContext = {
|
|
76
|
+
client,
|
|
77
|
+
serverCapabilities,
|
|
78
|
+
target: replayTarget,
|
|
79
|
+
timeoutMs: 10_000,
|
|
80
|
+
stderrLines: [],
|
|
81
|
+
};
|
|
82
|
+
const toolsCheck = await runToolsCheck(checkContext);
|
|
83
|
+
const promptsCheck = await runPromptsCheck(checkContext);
|
|
84
|
+
const resourcesCheck = await runResourcesCheck(checkContext);
|
|
85
|
+
const invokeCheck = await runToolsInvokeCheck(checkContext);
|
|
86
|
+
await client.close();
|
|
87
|
+
const checks = [
|
|
88
|
+
toolsCheck.result,
|
|
89
|
+
promptsCheck.result,
|
|
90
|
+
resourcesCheck.result,
|
|
91
|
+
invokeCheck.result,
|
|
92
|
+
];
|
|
93
|
+
const failCount = checks.filter((ch) => ch.status === "fail").length;
|
|
94
|
+
const gate = failCount > 0 ? "fail" : "pass";
|
|
95
|
+
const artifact = {
|
|
96
|
+
artifactType: "run",
|
|
97
|
+
schemaVersion: SCHEMA_VERSION,
|
|
98
|
+
gate,
|
|
99
|
+
runId: buildRunId(),
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
toolVersion: TOOL_VERSION,
|
|
102
|
+
target: {
|
|
103
|
+
targetId: cassette.targetId,
|
|
104
|
+
adapter: "local-process",
|
|
105
|
+
command: "replay",
|
|
106
|
+
args: [],
|
|
107
|
+
metadata: { source: "cassette", cassettePath },
|
|
108
|
+
},
|
|
109
|
+
environment: {
|
|
110
|
+
platform: `${os.platform()} ${os.release()}`,
|
|
111
|
+
nodeVersion: process.version,
|
|
112
|
+
},
|
|
113
|
+
summary: {
|
|
114
|
+
total: checks.length,
|
|
115
|
+
pass: checks.filter((ch) => ch.status === "pass").length,
|
|
116
|
+
fail: failCount,
|
|
117
|
+
partial: checks.filter((ch) => ch.status === "partial").length,
|
|
118
|
+
unsupported: checks.filter((ch) => ch.status === "unsupported").length,
|
|
119
|
+
flaky: checks.filter((ch) => ch.status === "flaky").length,
|
|
120
|
+
skipped: checks.filter((ch) => ch.status === "skipped").length,
|
|
121
|
+
gate,
|
|
122
|
+
},
|
|
123
|
+
checks,
|
|
124
|
+
};
|
|
125
|
+
process.stdout.write(`\n${renderTerminal(artifact)}\n`);
|
|
126
|
+
process.stdout.write(`\n${c(ANSI.dim, `Replayed from: ${cassettePath}`)}\n\n`);
|
|
127
|
+
if (artifact.gate === "fail") {
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// ── verify ─────────────────────────────────────────────────────────────
|
|
132
|
+
program
|
|
133
|
+
.command("verify")
|
|
134
|
+
.passThroughOptions()
|
|
135
|
+
.description("Verify a live server still matches a recorded cassette.")
|
|
136
|
+
.argument("<cassette>", "Path to a cassette JSON file.")
|
|
137
|
+
.argument("[command...]", "Server command and arguments to run.")
|
|
138
|
+
.option("--target <config>", "Path to a target config JSON file.")
|
|
139
|
+
.option("--no-color", "Disable colored output.")
|
|
140
|
+
.action(async (cassettePath, commandArgs, options) => {
|
|
141
|
+
const cassette = await loadCassette(cassettePath);
|
|
142
|
+
const target = options.target
|
|
143
|
+
? await readTargetConfig(options.target)
|
|
144
|
+
: targetFromCommand(commandArgs.length > 0 ? commandArgs : getPassthroughArgs());
|
|
145
|
+
process.stdout.write(`${c(ANSI.dim, "⟳")} Verifying ${c(ANSI.bold, target.targetId)} against cassette...\n`);
|
|
146
|
+
const { cassetteEntries } = await runTargetRecording(target, { invokeTools: true });
|
|
147
|
+
if (!cassetteEntries) {
|
|
148
|
+
process.stdout.write(`${c(ANSI.red, "✗")} Failed to record live session for comparison.\n`);
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const verifyResult = compareResponses(cassette, cassetteEntries);
|
|
153
|
+
process.stdout.write("\n");
|
|
154
|
+
for (const entry of verifyResult.entries) {
|
|
155
|
+
if (entry.status === "pass") {
|
|
156
|
+
process.stdout.write(` ${c(ANSI.green, "✓")} ${entry.method}\n`);
|
|
157
|
+
}
|
|
158
|
+
else if (entry.status === "fail") {
|
|
159
|
+
process.stdout.write(` ${c(ANSI.red, "✗")} ${entry.method}\n`);
|
|
160
|
+
if (entry.diff) {
|
|
161
|
+
for (const line of entry.diff.split("\n")) {
|
|
162
|
+
process.stdout.write(` ${c(ANSI.dim, line)}\n`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
process.stdout.write(` ${c(ANSI.yellow, "?")} ${entry.method} ${c(ANSI.dim, "(missing — server did not respond)")}\n`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
process.stdout.write("\n");
|
|
171
|
+
if (verifyResult.failed === 0 && verifyResult.missing === 0) {
|
|
172
|
+
process.stdout.write(c(ANSI.green, ` ✓ All ${verifyResult.passed} responses match cassette\n`));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
process.stdout.write(c(ANSI.red, ` ✗ ${verifyResult.failed} changed, ${verifyResult.missing} missing out of ${verifyResult.totalEntries} responses\n`));
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
process.stdout.write("\n");
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=record-replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-replay.js","sourceRoot":"","sources":["../../../src/commands/record-replay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { scanForTargets } from "../discovery.js";
|
|
3
|
+
import { runTarget, } from "../index.js";
|
|
4
|
+
import { TOOL_VERSION } from "../version.js";
|
|
5
|
+
import { ANSI, LOGO, c, useColor } from "./helpers.js";
|
|
6
|
+
// ── Scan implementation ─────────────────────────────────────────────────────
|
|
7
|
+
async function runScan(bin, configPath, invokeTools, securityCheck) {
|
|
8
|
+
process.stdout.write(useColor() ? c(ANSI.cyan, LOGO) + ` ${c(ANSI.dim, `v${TOOL_VERSION}`)}\n\n` : LOGO + ` v${TOOL_VERSION}\n\n`);
|
|
9
|
+
if (configPath) {
|
|
10
|
+
try {
|
|
11
|
+
await access(configPath);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
process.stdout.write(c(ANSI.red, ` ✗ Config file not found: ${configPath}\n\n`));
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const targets = await scanForTargets(configPath);
|
|
20
|
+
if (targets.length === 0) {
|
|
21
|
+
process.stdout.write(c(ANSI.yellow, " No MCP servers found.\n\n"));
|
|
22
|
+
process.stdout.write(c(ANSI.dim, " Looked in ~/.claude.json, Claude Desktop config, .mcp.json\n\n"));
|
|
23
|
+
process.stdout.write(" Test a specific server:\n");
|
|
24
|
+
process.stdout.write(` ${c(ANSI.dim, "$")} ${c(ANSI.cyan, `${bin} test npx -y @modelcontextprotocol/server-filesystem .`)}\n\n`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(c(ANSI.bold, ` Found ${targets.length} MCP server${targets.length === 1 ? "" : "s"}:\n`));
|
|
28
|
+
for (const t of targets) {
|
|
29
|
+
process.stdout.write(` ${c(ANSI.cyan, "●")} ${c(ANSI.bold, t.config.targetId)} ${c(ANSI.dim, `← ${t.source}`)}\n`);
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write("\n");
|
|
32
|
+
const results = [];
|
|
33
|
+
let passCount = 0;
|
|
34
|
+
let failCount = 0;
|
|
35
|
+
let totalTools = 0;
|
|
36
|
+
let totalPrompts = 0;
|
|
37
|
+
let totalResources = 0;
|
|
38
|
+
for (const t of targets) {
|
|
39
|
+
process.stdout.write(` ${c(ANSI.dim, "⟳")} Checking ${c(ANSI.bold, t.config.targetId)}...`);
|
|
40
|
+
try {
|
|
41
|
+
const artifact = await runTarget(t.config, { invokeTools, securityCheck });
|
|
42
|
+
const toolsCheck = artifact.checks.find((ch) => ch.id === "tools");
|
|
43
|
+
const promptsCheck = artifact.checks.find((ch) => ch.id === "prompts");
|
|
44
|
+
const resourcesCheck = artifact.checks.find((ch) => ch.id === "resources");
|
|
45
|
+
const toolCount = toolsCheck?.evidence[0]?.itemCount ?? 0;
|
|
46
|
+
const promptCount = promptsCheck?.evidence[0]?.itemCount ?? 0;
|
|
47
|
+
const resourceCount = resourcesCheck?.evidence[0]?.itemCount ?? 0;
|
|
48
|
+
totalTools += toolCount;
|
|
49
|
+
totalPrompts += promptCount;
|
|
50
|
+
totalResources += resourceCount;
|
|
51
|
+
const diagnostics = [];
|
|
52
|
+
for (const check of artifact.checks) {
|
|
53
|
+
if (check.status === "fail" || check.status === "partial") {
|
|
54
|
+
diagnostics.push(`${check.id}: ${check.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const gateIcon = artifact.gate === "pass" ? c(ANSI.green, " ✓") : c(ANSI.red, " ✗");
|
|
58
|
+
process.stdout.write(`\r ${gateIcon} ${c(ANSI.bold, t.config.targetId)}${" ".repeat(Math.max(1, 40 - t.config.targetId.length))}`);
|
|
59
|
+
process.stdout.write(`${c(ANSI.dim, `${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`)}\n`);
|
|
60
|
+
if (artifact.fatalError) {
|
|
61
|
+
process.stdout.write(` ${c(ANSI.red, "→")} ${artifact.fatalError.split("\n")[0]}\n`);
|
|
62
|
+
}
|
|
63
|
+
else if (artifact.gate === "fail" && diagnostics.length > 0) {
|
|
64
|
+
process.stdout.write(` ${c(ANSI.dim, "→")} ${diagnostics[0]}\n`);
|
|
65
|
+
}
|
|
66
|
+
results.push({ targetId: t.config.targetId, gate: artifact.gate, toolCount, promptCount, resourceCount, diagnostics });
|
|
67
|
+
if (artifact.gate === "pass")
|
|
68
|
+
passCount++;
|
|
69
|
+
else
|
|
70
|
+
failCount++;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
74
|
+
let friendlyMsg = msg;
|
|
75
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
76
|
+
const cmd = t.config.adapter === "http" ? t.config.url : t.config.command;
|
|
77
|
+
friendlyMsg = `Could not start server — "${cmd}" not found. Is it installed?`;
|
|
78
|
+
}
|
|
79
|
+
else if (msg.includes("ECONNREFUSED")) {
|
|
80
|
+
friendlyMsg = `Server is not running or refused the connection.`;
|
|
81
|
+
}
|
|
82
|
+
else if (msg.includes("timed out") || msg.includes("timeout")) {
|
|
83
|
+
friendlyMsg = `Server took too long to respond.`;
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write(`\r ${c(ANSI.red, "✗")} ${c(ANSI.bold, t.config.targetId)}\n`);
|
|
86
|
+
process.stdout.write(` ${c(ANSI.red, friendlyMsg)}\n`);
|
|
87
|
+
results.push({ targetId: t.config.targetId, gate: "fail", toolCount: 0, promptCount: 0, resourceCount: 0, error: friendlyMsg, diagnostics: [] });
|
|
88
|
+
failCount++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ── Summary ──────────────────────────────────────────────────────────
|
|
92
|
+
process.stdout.write("\n");
|
|
93
|
+
if (failCount === 0) {
|
|
94
|
+
process.stdout.write(c(ANSI.green, ` ✓ All ${passCount} server${passCount === 1 ? "" : "s"} healthy`));
|
|
95
|
+
process.stdout.write(c(ANSI.dim, ` — ${totalTools} tools, ${totalPrompts} prompts, ${totalResources} resources\n`));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
process.stdout.write(c(ANSI.red, ` ✗ ${failCount} of ${passCount + failCount} server${passCount + failCount === 1 ? "" : "s"} failing`));
|
|
99
|
+
if (totalTools > 0 || totalPrompts > 0 || totalResources > 0) {
|
|
100
|
+
process.stdout.write(c(ANSI.dim, ` — ${totalTools} tools, ${totalPrompts} prompts, ${totalResources} resources found\n`));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
process.stdout.write("\n");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Show diagnostics for failures or notable partials
|
|
107
|
+
const issues = results.filter((r) => r.diagnostics.length > 0 && !r.error);
|
|
108
|
+
if (issues.length > 0) {
|
|
109
|
+
process.stdout.write("\n");
|
|
110
|
+
for (const r of issues) {
|
|
111
|
+
process.stdout.write(` ${c(ANSI.yellow, r.targetId)}:\n`);
|
|
112
|
+
for (const d of r.diagnostics.slice(0, 3)) {
|
|
113
|
+
process.stdout.write(` ${c(ANSI.dim, "→")} ${d}\n`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ── Next step ────────────────────────────────────────────────────────
|
|
118
|
+
process.stdout.write("\n");
|
|
119
|
+
if (!invokeTools && totalTools > 0) {
|
|
120
|
+
process.stdout.write(c(ANSI.dim, ` Next: ${c(ANSI.cyan, `${bin} scan deep`)} to also test that tools run\n`));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
process.stdout.write(c(ANSI.dim, ` Run ${c(ANSI.cyan, `${bin} --help`)} for more commands\n`));
|
|
124
|
+
}
|
|
125
|
+
process.stdout.write("\n");
|
|
126
|
+
if (failCount > 0) {
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Register ────────────────────────────────────────────────────────────────
|
|
131
|
+
export function registerScanCommands(program, bin) {
|
|
132
|
+
const scanCmd = program
|
|
133
|
+
.command("scan")
|
|
134
|
+
.description("Check all MCP servers in your Claude configs.")
|
|
135
|
+
.option("--config <path>", "Path to a specific MCP config file.")
|
|
136
|
+
.option("--security", "Run security analysis on tool schemas.")
|
|
137
|
+
.option("--no-color", "Disable colored output.");
|
|
138
|
+
// `scan` with no subcommand — basic scan
|
|
139
|
+
scanCmd.action(async (options) => {
|
|
140
|
+
await runScan(bin, options.config, false, options.security);
|
|
141
|
+
});
|
|
142
|
+
// `scan deep` — scan + invoke tools
|
|
143
|
+
scanCmd
|
|
144
|
+
.command("deep")
|
|
145
|
+
.description("Scan and also invoke safe tools to verify they execute.")
|
|
146
|
+
.option("--config <path>", "Path to a specific MCP config file.")
|
|
147
|
+
.option("--security", "Run security analysis on tool schemas.")
|
|
148
|
+
.action(async (options) => {
|
|
149
|
+
// Inherit parent config option if set
|
|
150
|
+
const parentConfig = scanCmd.opts().config;
|
|
151
|
+
const parentSecurity = scanCmd.opts().security;
|
|
152
|
+
await runScan(bin, options.config ?? parentConfig, true, options.security ?? parentSecurity ?? true);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../src/commands/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { generateBadgeSvg } from "../badge.js";
|
|
4
|
+
import { runTarget, writeRunArtifact, } from "../index.js";
|
|
5
|
+
import { defaultRunsDirectory } from "../storage.js";
|
|
6
|
+
import { ANSI, c, formatOutput, targetFromCommand, writeOutput } from "./helpers.js";
|
|
7
|
+
export function registerScoreCommands(program) {
|
|
8
|
+
// ── score ────────────────────────────────────────────────────────────
|
|
9
|
+
program
|
|
10
|
+
.command("score")
|
|
11
|
+
.passThroughOptions()
|
|
12
|
+
.description("Score an MCP server's health (0-100).")
|
|
13
|
+
.argument("<command...>", "Server command and arguments to run.")
|
|
14
|
+
.option("--format <format>", "terminal, json, junit, markdown, html, or sarif", "terminal")
|
|
15
|
+
.option("--output <file>", "Write to file instead of stdout.")
|
|
16
|
+
.option("--no-color", "Disable colored output.")
|
|
17
|
+
.action(async (commandArgs, options) => {
|
|
18
|
+
const target = targetFromCommand(commandArgs);
|
|
19
|
+
process.stdout.write(`${c(ANSI.dim, "⟳")} Scoring ${c(ANSI.bold, target.targetId)}...\n\n`);
|
|
20
|
+
const artifact = await runTarget(target, { invokeTools: true, securityCheck: true });
|
|
21
|
+
await writeRunArtifact(artifact, defaultRunsDirectory(process.cwd()));
|
|
22
|
+
if (options.format !== "terminal") {
|
|
23
|
+
const output = formatOutput(artifact, options.format);
|
|
24
|
+
await writeOutput(output, options.format, options.output);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const score = artifact.healthScore;
|
|
28
|
+
if (!score) {
|
|
29
|
+
process.stdout.write(" Could not compute health score.\n\n");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const gradeColor = score.grade === "A" || score.grade === "B" ? ANSI.green
|
|
33
|
+
: score.grade === "C" ? ANSI.yellow
|
|
34
|
+
: ANSI.red;
|
|
35
|
+
process.stdout.write(c(ANSI.bold, ` MCP Health Score: ${c(gradeColor, `${score.overall}/100`)} (${c(gradeColor, score.grade)})\n\n`));
|
|
36
|
+
for (const dim of score.dimensions) {
|
|
37
|
+
const filled = Math.round(dim.score / 5);
|
|
38
|
+
const empty = 20 - filled;
|
|
39
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
40
|
+
const dimColor = dim.score >= 80 ? ANSI.green : dim.score >= 60 ? ANSI.yellow : ANSI.red;
|
|
41
|
+
const weightPct = Math.round(dim.weight * 100);
|
|
42
|
+
process.stdout.write(` ${dim.name.padEnd(22)} ${c(dimColor, bar)} ${String(dim.score).padStart(3)} ${c(ANSI.dim, `(weight: ${weightPct}%)`)}\n`);
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write("\n");
|
|
45
|
+
// Show details for dimensions that aren't perfect
|
|
46
|
+
for (const dim of score.dimensions) {
|
|
47
|
+
if (dim.score < 100 && dim.details.length > 0) {
|
|
48
|
+
process.stdout.write(` ${c(ANSI.dim, dim.name + ":")}\n`);
|
|
49
|
+
for (const detail of dim.details) {
|
|
50
|
+
process.stdout.write(` ${c(ANSI.dim, "→")} ${detail}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
process.stdout.write("\n");
|
|
55
|
+
if (artifact.gate === "fail") {
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// ── badge ───────────────────────────────────────────────────────────
|
|
60
|
+
program
|
|
61
|
+
.command("badge")
|
|
62
|
+
.passThroughOptions()
|
|
63
|
+
.description("Generate an SVG health score badge for your README.")
|
|
64
|
+
.argument("<command...>", "Server command and arguments to run.")
|
|
65
|
+
.option("--output <file>", "Write SVG to file (default: stdout).")
|
|
66
|
+
.option("--label <text>", "Badge label text.", "MCP Health")
|
|
67
|
+
.action(async (commandArgs, options) => {
|
|
68
|
+
const target = targetFromCommand(commandArgs);
|
|
69
|
+
process.stderr.write(`${c(ANSI.dim, "⟳")} Scoring ${c(ANSI.bold, target.targetId)}...\n`);
|
|
70
|
+
const artifact = await runTarget(target, { invokeTools: true, securityCheck: true });
|
|
71
|
+
const score = artifact.healthScore;
|
|
72
|
+
if (!score) {
|
|
73
|
+
process.stderr.write(" Could not compute health score.\n");
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const svg = generateBadgeSvg({ score: score.overall, grade: score.grade, label: options.label });
|
|
78
|
+
if (options.output) {
|
|
79
|
+
await mkdir(path.dirname(options.output), { recursive: true });
|
|
80
|
+
await writeFile(options.output, svg, "utf8");
|
|
81
|
+
process.stderr.write(` Badge written to ${options.output}\n`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
process.stdout.write(svg);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../../../src/commands/score.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function registerServeCommands(program) {
|
|
2
|
+
program
|
|
3
|
+
.command("serve")
|
|
4
|
+
.description("Start as an MCP server for AI agents.")
|
|
5
|
+
.action(async () => {
|
|
6
|
+
const { startServer } = await import("../server.js");
|
|
7
|
+
await startServer();
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/commands/serve.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,uCAAuC,CAAC;SACpD,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACrD,MAAM,WAAW,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { scanForTargets } from "../discovery.js";
|
|
2
|
+
import { detectEnvironment } from "../environment.js";
|
|
3
|
+
import { ANSI, c } from "./helpers.js";
|
|
4
|
+
export function registerSuggestCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("suggest")
|
|
7
|
+
.description("Detect your stack and recommend MCP servers.")
|
|
8
|
+
.option("--cwd <path>", "Directory to scan for project signals.", process.cwd())
|
|
9
|
+
.option("--no-color", "Disable colored output.")
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
process.stdout.write(`${c(ANSI.dim, "⟳")} Scanning environment...\n\n`);
|
|
12
|
+
// 1. Current MCP servers
|
|
13
|
+
const targets = await scanForTargets();
|
|
14
|
+
if (targets.length > 0) {
|
|
15
|
+
process.stdout.write(c(ANSI.bold, " Configured MCP Servers\n"));
|
|
16
|
+
for (const t of targets) {
|
|
17
|
+
const detail = t.config.adapter === "http"
|
|
18
|
+
? t.config.url
|
|
19
|
+
: `${t.config.command} ${t.config.args.join(" ")}`;
|
|
20
|
+
process.stdout.write(` ${c(ANSI.cyan, "●")} ${c(ANSI.bold, t.config.targetId)} ${c(ANSI.dim, detail)} ${c(ANSI.dim, `← ${t.source}`)}\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
process.stdout.write(` ${c(ANSI.yellow, "No MCP servers configured.")}\n`);
|
|
25
|
+
}
|
|
26
|
+
process.stdout.write("\n");
|
|
27
|
+
// 2. Environment detection
|
|
28
|
+
const env = await detectEnvironment(options.cwd);
|
|
29
|
+
const hasSignals = env.languages.length > 0 || env.frameworks.length > 0 || env.databases.length > 0;
|
|
30
|
+
if (hasSignals) {
|
|
31
|
+
process.stdout.write(c(ANSI.bold, " Detected Stack\n"));
|
|
32
|
+
if (env.languages.length > 0)
|
|
33
|
+
process.stdout.write(` ${c(ANSI.dim, "Languages:")} ${env.languages.join(", ")}\n`);
|
|
34
|
+
if (env.frameworks.length > 0)
|
|
35
|
+
process.stdout.write(` ${c(ANSI.dim, "Frameworks:")} ${env.frameworks.join(", ")}\n`);
|
|
36
|
+
if (env.databases.length > 0)
|
|
37
|
+
process.stdout.write(` ${c(ANSI.dim, "Databases:")} ${env.databases.join(", ")}\n`);
|
|
38
|
+
if (env.cloud.length > 0)
|
|
39
|
+
process.stdout.write(` ${c(ANSI.dim, "Cloud:")} ${env.cloud.join(", ")}\n`);
|
|
40
|
+
if (env.cicd.length > 0)
|
|
41
|
+
process.stdout.write(` ${c(ANSI.dim, "CI/CD:")} ${env.cicd.join(", ")}\n`);
|
|
42
|
+
if (env.services.length > 0)
|
|
43
|
+
process.stdout.write(` ${c(ANSI.dim, "Services:")} ${env.services.join(", ")}\n`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
process.stdout.write(` ${c(ANSI.dim, "No recognizable project signals in")} ${options.cwd}\n`);
|
|
47
|
+
}
|
|
48
|
+
process.stdout.write("\n");
|
|
49
|
+
// 3. MCP Registry — filtered by detected stack
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch("https://registry.modelcontextprotocol.io/v0/servers", {
|
|
52
|
+
signal: AbortSignal.timeout(10_000),
|
|
53
|
+
headers: { "Accept": "application/json" },
|
|
54
|
+
});
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
const raw = Array.isArray(data) ? data : (typeof data === "object" && data !== null
|
|
58
|
+
? (data["servers"] ?? data["results"] ?? data["items"])
|
|
59
|
+
: null);
|
|
60
|
+
if (Array.isArray(raw)) {
|
|
61
|
+
const allEntries = raw;
|
|
62
|
+
// Build keyword set from detected environment
|
|
63
|
+
const keywords = new Set([
|
|
64
|
+
...env.languages, ...env.frameworks, ...env.databases,
|
|
65
|
+
...env.services, ...env.cloud, ...env.cicd,
|
|
66
|
+
].map(s => s.toLowerCase()));
|
|
67
|
+
// Also add common aliases
|
|
68
|
+
const aliases = {
|
|
69
|
+
typescript: ["ts"], javascript: ["js", "node"], python: ["py"],
|
|
70
|
+
postgresql: ["postgres"], mongodb: ["mongo"], github: ["gh"],
|
|
71
|
+
};
|
|
72
|
+
for (const kw of [...keywords]) {
|
|
73
|
+
for (const [full, abbrs] of Object.entries(aliases)) {
|
|
74
|
+
if (kw === full)
|
|
75
|
+
for (const a of abbrs)
|
|
76
|
+
keywords.add(a);
|
|
77
|
+
if (abbrs.includes(kw))
|
|
78
|
+
keywords.add(full);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Score each entry against detected stack
|
|
82
|
+
const scored = allEntries.map(entry => {
|
|
83
|
+
const srv = (typeof entry["server"] === "object" && entry["server"] !== null ? entry["server"] : entry);
|
|
84
|
+
const name = typeof srv["name"] === "string" ? srv["name"] : (typeof entry["name"] === "string" ? entry["name"] : "unknown");
|
|
85
|
+
const desc = typeof srv["description"] === "string" ? srv["description"] : (typeof entry["description"] === "string" ? entry["description"] : "");
|
|
86
|
+
const text = `${name} ${desc}`.toLowerCase();
|
|
87
|
+
const matches = [...keywords].filter(k => text.includes(k)).length;
|
|
88
|
+
return { name, desc, matches };
|
|
89
|
+
});
|
|
90
|
+
const recommended = scored.filter(s => s.matches > 0).sort((a, b) => b.matches - a.matches);
|
|
91
|
+
const others = scored.filter(s => s.matches === 0);
|
|
92
|
+
if (recommended.length > 0) {
|
|
93
|
+
process.stdout.write(c(ANSI.bold, " Recommended for Your Stack\n"));
|
|
94
|
+
for (const r of recommended.slice(0, 10)) {
|
|
95
|
+
process.stdout.write(` ${c(ANSI.green, "★")} ${c(ANSI.bold, r.name)}${r.desc ? ` ${c(ANSI.dim, "—")} ${r.desc}` : ""}\n`);
|
|
96
|
+
}
|
|
97
|
+
if (recommended.length > 10) {
|
|
98
|
+
process.stdout.write(` ${c(ANSI.dim, `... and ${recommended.length - 10} more matches`)}\n`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
process.stdout.write(c(ANSI.bold, " MCP Registry\n"));
|
|
103
|
+
for (const s of scored.slice(0, 10)) {
|
|
104
|
+
process.stdout.write(` ${c(ANSI.dim, "●")} ${c(ANSI.bold, s.name)}${s.desc ? ` ${c(ANSI.dim, "—")} ${s.desc}` : ""}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (others.length > 0) {
|
|
108
|
+
process.stdout.write(`\n ${c(ANSI.dim, `${others.length} more servers at registry.modelcontextprotocol.io`)}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
process.stdout.write(` ${c(ANSI.dim, "Registry returned unexpected format.")}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
process.stdout.write(` ${c(ANSI.dim, `Registry returned HTTP ${response.status}`)}\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
121
|
+
process.stdout.write(` ${c(ANSI.yellow, "Could not reach registry:")} ${msg}\n`);
|
|
122
|
+
}
|
|
123
|
+
process.stdout.write("\n");
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=suggest.js.map
|