@mcp-guardian/cli 4.1.2
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/.turbo/turbo-build.log +9 -0
- package/.turbo/turbo-test.log +49 -0
- package/README.md +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +140 -0
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +430 -0
- package/dist/tui/data-fetcher.d.ts +144 -0
- package/dist/tui/data-fetcher.js +268 -0
- package/package.json +20 -0
- package/package.json.prepack-backup +28 -0
- package/src/index.ts +156 -0
- package/tests/index.test.ts +122 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export interface TuiData {
|
|
2
|
+
overview: OverviewData;
|
|
3
|
+
security: SecurityData;
|
|
4
|
+
cost: CostData;
|
|
5
|
+
health: HealthData;
|
|
6
|
+
ai: AiData;
|
|
7
|
+
audit: AuditData;
|
|
8
|
+
policy: PolicyData;
|
|
9
|
+
instances: InstanceData[];
|
|
10
|
+
}
|
|
11
|
+
export interface OverviewData {
|
|
12
|
+
totalInstances: number;
|
|
13
|
+
activeInstances: number;
|
|
14
|
+
totalRequests: number;
|
|
15
|
+
blockedRequests: number;
|
|
16
|
+
passRate: number;
|
|
17
|
+
totalCostUsd: number;
|
|
18
|
+
burnRatePerHour: number;
|
|
19
|
+
avgLatencyMs: number;
|
|
20
|
+
activeServers: number;
|
|
21
|
+
lastUpdated: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SecurityData {
|
|
24
|
+
servers: {
|
|
25
|
+
name: string;
|
|
26
|
+
score: number;
|
|
27
|
+
cves: number;
|
|
28
|
+
critical: number;
|
|
29
|
+
auth: boolean;
|
|
30
|
+
}[];
|
|
31
|
+
overallScore: number;
|
|
32
|
+
worstOffenders: string[];
|
|
33
|
+
activeThreats: number;
|
|
34
|
+
lastScan: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CostData {
|
|
37
|
+
servers: {
|
|
38
|
+
name: string;
|
|
39
|
+
tokens: number;
|
|
40
|
+
cost: number;
|
|
41
|
+
trend: string;
|
|
42
|
+
}[];
|
|
43
|
+
totalCost: number;
|
|
44
|
+
projectedMonthly: number;
|
|
45
|
+
budgetAlerts: string[];
|
|
46
|
+
pricingModel: string;
|
|
47
|
+
}
|
|
48
|
+
export interface HealthData {
|
|
49
|
+
servers: {
|
|
50
|
+
name: string;
|
|
51
|
+
latency: number;
|
|
52
|
+
successRate: number;
|
|
53
|
+
tools: number;
|
|
54
|
+
circuitBreaker: string;
|
|
55
|
+
}[];
|
|
56
|
+
atRisk: string[];
|
|
57
|
+
avgLatency: number;
|
|
58
|
+
totalTools: number;
|
|
59
|
+
}
|
|
60
|
+
export interface AiData {
|
|
61
|
+
suggestions: any[];
|
|
62
|
+
baselines: any[];
|
|
63
|
+
learningState: {
|
|
64
|
+
adaptiveThreshold: number;
|
|
65
|
+
truePositiveRate: number;
|
|
66
|
+
falsePositiveRate: number;
|
|
67
|
+
moduleWeights: Record<string, number>;
|
|
68
|
+
};
|
|
69
|
+
threats: any[];
|
|
70
|
+
report: any;
|
|
71
|
+
}
|
|
72
|
+
export interface AuditData {
|
|
73
|
+
events: any[];
|
|
74
|
+
total: number;
|
|
75
|
+
blocked: number;
|
|
76
|
+
passed: number;
|
|
77
|
+
flagged: number;
|
|
78
|
+
}
|
|
79
|
+
export interface PolicyData {
|
|
80
|
+
mode: string;
|
|
81
|
+
activeRules: number;
|
|
82
|
+
autoGeneratedRules: string[];
|
|
83
|
+
rules: any[];
|
|
84
|
+
}
|
|
85
|
+
export interface InstanceData {
|
|
86
|
+
instanceId: string;
|
|
87
|
+
instanceName: string;
|
|
88
|
+
status: string;
|
|
89
|
+
hostname: string;
|
|
90
|
+
version: string;
|
|
91
|
+
lastHeartbeat: string;
|
|
92
|
+
totalRequests: number;
|
|
93
|
+
blockedRequests: number;
|
|
94
|
+
totalCostUsd: number;
|
|
95
|
+
avgLatencyMs: number;
|
|
96
|
+
}
|
|
97
|
+
export declare class DataFetcher {
|
|
98
|
+
private dashboardUrl;
|
|
99
|
+
private ws;
|
|
100
|
+
private wsUrl;
|
|
101
|
+
private cache;
|
|
102
|
+
private listeners;
|
|
103
|
+
private wsConnected;
|
|
104
|
+
private reconnectTimer;
|
|
105
|
+
constructor(dashboardUrl?: string);
|
|
106
|
+
/** Get current cached data */
|
|
107
|
+
getData(): TuiData | null;
|
|
108
|
+
/** Subscribe to data updates */
|
|
109
|
+
onChange(callback: () => void): () => void;
|
|
110
|
+
/** Notify all listeners */
|
|
111
|
+
private notify;
|
|
112
|
+
/** Connect WebSocket for real-time updates */
|
|
113
|
+
connectWebSocket(): void;
|
|
114
|
+
/** Fall back to HTTP polling when WebSocket is unavailable */
|
|
115
|
+
private fallbackToHttp;
|
|
116
|
+
/** Schedule WebSocket reconnection */
|
|
117
|
+
private scheduleReconnect;
|
|
118
|
+
/** Handle incoming WebSocket messages */
|
|
119
|
+
private handleWsMessage;
|
|
120
|
+
/** Fetch all data from HTTP API */
|
|
121
|
+
fetchAll(): Promise<TuiData>;
|
|
122
|
+
/** Fetch JSON from an API endpoint */
|
|
123
|
+
private fetchJson;
|
|
124
|
+
/** Start periodic HTTP polling fallback */
|
|
125
|
+
startPolling(intervalMs?: number): ReturnType<typeof setInterval>;
|
|
126
|
+
/** Parse overview data */
|
|
127
|
+
private parseOverview;
|
|
128
|
+
/** Parse security data */
|
|
129
|
+
private parseSecurity;
|
|
130
|
+
/** Parse cost data */
|
|
131
|
+
private parseCost;
|
|
132
|
+
/** Parse health data */
|
|
133
|
+
private parseHealth;
|
|
134
|
+
/** Parse AI data */
|
|
135
|
+
private parseAi;
|
|
136
|
+
/** Parse audit data */
|
|
137
|
+
private parseAudit;
|
|
138
|
+
/** Parse policy data */
|
|
139
|
+
private parsePolicy;
|
|
140
|
+
/** Parse instances data */
|
|
141
|
+
private parseInstances;
|
|
142
|
+
/** Stop and clean up */
|
|
143
|
+
stop(): void;
|
|
144
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
export class DataFetcher {
|
|
2
|
+
dashboardUrl;
|
|
3
|
+
ws = null;
|
|
4
|
+
wsUrl;
|
|
5
|
+
cache = null;
|
|
6
|
+
listeners = new Set();
|
|
7
|
+
wsConnected = false;
|
|
8
|
+
reconnectTimer = null;
|
|
9
|
+
constructor(dashboardUrl) {
|
|
10
|
+
this.dashboardUrl = (dashboardUrl || process.env['GUARDIAN_DASHBOARD_URL'] || 'http://localhost:4000').replace(/\/$/, '');
|
|
11
|
+
this.wsUrl = this.dashboardUrl.replace(/^http/, 'ws') + '/ws';
|
|
12
|
+
}
|
|
13
|
+
/** Get current cached data */
|
|
14
|
+
getData() {
|
|
15
|
+
return this.cache;
|
|
16
|
+
}
|
|
17
|
+
/** Subscribe to data updates */
|
|
18
|
+
onChange(callback) {
|
|
19
|
+
this.listeners.add(callback);
|
|
20
|
+
return () => this.listeners.delete(callback);
|
|
21
|
+
}
|
|
22
|
+
/** Notify all listeners */
|
|
23
|
+
notify() {
|
|
24
|
+
for (const listener of this.listeners) {
|
|
25
|
+
try {
|
|
26
|
+
listener();
|
|
27
|
+
}
|
|
28
|
+
catch { /* ignore listener errors */ }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Connect WebSocket for real-time updates */
|
|
32
|
+
connectWebSocket() {
|
|
33
|
+
try {
|
|
34
|
+
// Using global WebSocket available in Node 22+
|
|
35
|
+
const WS = globalThis.WebSocket;
|
|
36
|
+
if (!WS) {
|
|
37
|
+
this.fallbackToHttp();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.ws = new WS(this.wsUrl);
|
|
41
|
+
this.ws.onopen = () => {
|
|
42
|
+
this.wsConnected = true;
|
|
43
|
+
// Subscribe to all channels
|
|
44
|
+
if (this.ws?.readyState === 1) {
|
|
45
|
+
this.ws.send(JSON.stringify({
|
|
46
|
+
type: 'subscribe',
|
|
47
|
+
channels: ['ai', 'audit', 'metrics', 'logs', 'instances', 'policy', 'health', 'cost'],
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
this.ws.onmessage = (event) => {
|
|
52
|
+
try {
|
|
53
|
+
const msg = JSON.parse(event.data.toString());
|
|
54
|
+
this.handleWsMessage(msg);
|
|
55
|
+
}
|
|
56
|
+
catch { /* ignore parse errors */ }
|
|
57
|
+
};
|
|
58
|
+
this.ws.onclose = () => {
|
|
59
|
+
this.wsConnected = false;
|
|
60
|
+
this.scheduleReconnect();
|
|
61
|
+
};
|
|
62
|
+
this.ws.onerror = () => {
|
|
63
|
+
this.wsConnected = false;
|
|
64
|
+
this.fallbackToHttp();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
this.fallbackToHttp();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Fall back to HTTP polling when WebSocket is unavailable */
|
|
72
|
+
fallbackToHttp() {
|
|
73
|
+
this.fetchAll().catch(() => {
|
|
74
|
+
// Will retry on next poll interval
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Schedule WebSocket reconnection */
|
|
78
|
+
scheduleReconnect() {
|
|
79
|
+
if (this.reconnectTimer)
|
|
80
|
+
return;
|
|
81
|
+
this.reconnectTimer = setTimeout(() => {
|
|
82
|
+
this.reconnectTimer = null;
|
|
83
|
+
this.connectWebSocket();
|
|
84
|
+
}, 5000);
|
|
85
|
+
}
|
|
86
|
+
/** Handle incoming WebSocket messages */
|
|
87
|
+
handleWsMessage(msg) {
|
|
88
|
+
const cache = this.cache;
|
|
89
|
+
if (!cache)
|
|
90
|
+
return;
|
|
91
|
+
switch (msg.type) {
|
|
92
|
+
case 'ai:suggestions':
|
|
93
|
+
cache.ai.suggestions = msg.payload?.suggestions || [];
|
|
94
|
+
break;
|
|
95
|
+
case 'ai:baselines':
|
|
96
|
+
cache.ai.baselines = msg.payload?.baselines || [];
|
|
97
|
+
break;
|
|
98
|
+
case 'ai:state':
|
|
99
|
+
cache.ai.learningState = msg.payload?.state || cache.ai.learningState;
|
|
100
|
+
break;
|
|
101
|
+
case 'ai:threats':
|
|
102
|
+
cache.ai.threats = msg.payload?.threats || [];
|
|
103
|
+
break;
|
|
104
|
+
case 'ai:report':
|
|
105
|
+
cache.ai.report = msg.payload?.report || null;
|
|
106
|
+
break;
|
|
107
|
+
case 'audit:events':
|
|
108
|
+
if (msg.payload?.events) {
|
|
109
|
+
cache.audit.events = msg.payload.events;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case 'metrics:live':
|
|
113
|
+
if (msg.payload?.instances) {
|
|
114
|
+
cache.instances = msg.payload.instances;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
this.notify();
|
|
119
|
+
}
|
|
120
|
+
/** Fetch all data from HTTP API */
|
|
121
|
+
async fetchAll() {
|
|
122
|
+
const [overview, security, cost, health, ai, audit, policy, instances] = await Promise.allSettled([
|
|
123
|
+
this.fetchJson('/api/aggregate/metrics'),
|
|
124
|
+
this.fetchJson('/api/security'),
|
|
125
|
+
this.fetchJson('/api/cost'),
|
|
126
|
+
this.fetchJson('/api/health'),
|
|
127
|
+
this.fetchJson('/api/ai/report'),
|
|
128
|
+
this.fetchJson('/api/aggregate/audit'),
|
|
129
|
+
this.fetchJson('/api/policy'),
|
|
130
|
+
this.fetchJson('/api/instances'),
|
|
131
|
+
]);
|
|
132
|
+
this.cache = {
|
|
133
|
+
overview: this.parseOverview(overview.status === 'fulfilled' ? overview.value : null),
|
|
134
|
+
security: this.parseSecurity(security.status === 'fulfilled' ? security.value : null),
|
|
135
|
+
cost: this.parseCost(cost.status === 'fulfilled' ? cost.value : null),
|
|
136
|
+
health: this.parseHealth(health.status === 'fulfilled' ? health.value : null),
|
|
137
|
+
ai: this.parseAi(ai.status === 'fulfilled' ? ai.value : null),
|
|
138
|
+
audit: this.parseAudit(audit.status === 'fulfilled' ? audit.value : null),
|
|
139
|
+
policy: this.parsePolicy(policy.status === 'fulfilled' ? policy.value : null),
|
|
140
|
+
instances: this.parseInstances(instances.status === 'fulfilled' ? instances.value : null),
|
|
141
|
+
};
|
|
142
|
+
this.notify();
|
|
143
|
+
return this.cache;
|
|
144
|
+
}
|
|
145
|
+
/** Fetch JSON from an API endpoint */
|
|
146
|
+
async fetchJson(path) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(`${this.dashboardUrl}${path}`, {
|
|
149
|
+
signal: AbortSignal.timeout(5000),
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok)
|
|
152
|
+
return null;
|
|
153
|
+
return response.json();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Start periodic HTTP polling fallback */
|
|
160
|
+
startPolling(intervalMs = 3000) {
|
|
161
|
+
return setInterval(() => {
|
|
162
|
+
this.fetchAll().catch(() => { });
|
|
163
|
+
}, intervalMs);
|
|
164
|
+
}
|
|
165
|
+
/** Parse overview data */
|
|
166
|
+
parseOverview(data) {
|
|
167
|
+
return {
|
|
168
|
+
totalInstances: data?.totalInstances || 0,
|
|
169
|
+
activeInstances: data?.activeInstances || 0,
|
|
170
|
+
totalRequests: data?.totalRequests || 0,
|
|
171
|
+
blockedRequests: data?.blockedRequests || 0,
|
|
172
|
+
passRate: data?.totalRequests > 0 ? ((data.passedRequests || 0) / data.totalRequests) * 100 : 100,
|
|
173
|
+
totalCostUsd: data?.totalCost || 0,
|
|
174
|
+
burnRatePerHour: data?.burnRatePerHour || 0,
|
|
175
|
+
avgLatencyMs: data?.avgLatencyMs || 0,
|
|
176
|
+
activeServers: data?.activeServers || 0,
|
|
177
|
+
lastUpdated: data?.lastUpdated || new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Parse security data */
|
|
181
|
+
parseSecurity(data) {
|
|
182
|
+
return {
|
|
183
|
+
servers: data?.serverReports || [],
|
|
184
|
+
overallScore: data?.overallScore || 0,
|
|
185
|
+
worstOffenders: data?.worstOffenders || [],
|
|
186
|
+
activeThreats: data?.activeThreats || 0,
|
|
187
|
+
lastScan: data?.lastScan || 'N/A',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Parse cost data */
|
|
191
|
+
parseCost(data) {
|
|
192
|
+
return {
|
|
193
|
+
servers: data?.serverReports || [],
|
|
194
|
+
totalCost: data?.totalCost || 0,
|
|
195
|
+
projectedMonthly: data?.projectedMonthly || 0,
|
|
196
|
+
budgetAlerts: data?.budgetAlerts || [],
|
|
197
|
+
pricingModel: data?.pricingModel || 'unknown',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Parse health data */
|
|
201
|
+
parseHealth(data) {
|
|
202
|
+
return {
|
|
203
|
+
servers: data?.serverReports || [],
|
|
204
|
+
atRisk: data?.atRisk || [],
|
|
205
|
+
avgLatency: data?.avgLatency || 0,
|
|
206
|
+
totalTools: data?.totalTools || 0,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Parse AI data */
|
|
210
|
+
parseAi(data) {
|
|
211
|
+
return {
|
|
212
|
+
suggestions: data?.suggestions || [],
|
|
213
|
+
baselines: data?.baselines || [],
|
|
214
|
+
learningState: data?.learningState || { adaptiveThreshold: 0.85, truePositiveRate: 0, falsePositiveRate: 0, moduleWeights: {} },
|
|
215
|
+
threats: data?.threats || [],
|
|
216
|
+
report: data?.report || null,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Parse audit data */
|
|
220
|
+
parseAudit(data) {
|
|
221
|
+
return {
|
|
222
|
+
events: data?.events || [],
|
|
223
|
+
total: data?.total || 0,
|
|
224
|
+
blocked: data?.blocked || 0,
|
|
225
|
+
passed: data?.passed || 0,
|
|
226
|
+
flagged: data?.flagged || 0,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Parse policy data */
|
|
230
|
+
parsePolicy(data) {
|
|
231
|
+
return {
|
|
232
|
+
mode: data?.mode || 'none',
|
|
233
|
+
activeRules: data?.activeRules || 0,
|
|
234
|
+
autoGeneratedRules: data?.autoGeneratedRules || [],
|
|
235
|
+
rules: data?.rules || [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** Parse instances data */
|
|
239
|
+
parseInstances(data) {
|
|
240
|
+
if (Array.isArray(data)) {
|
|
241
|
+
return data.map((inst) => ({
|
|
242
|
+
instanceId: inst.instance_id || inst.instanceId || 'unknown',
|
|
243
|
+
instanceName: inst.instance_name || inst.instanceName || 'unknown',
|
|
244
|
+
status: inst.status || 'unknown',
|
|
245
|
+
hostname: inst.hostname || inst.host_name || 'unknown',
|
|
246
|
+
version: inst.version || 'unknown',
|
|
247
|
+
lastHeartbeat: inst.last_heartbeat || inst.lastHeartbeat || 'N/A',
|
|
248
|
+
totalRequests: inst.total_requests || inst.totalRequests || 0,
|
|
249
|
+
blockedRequests: inst.blocked_requests || inst.blockedRequests || 0,
|
|
250
|
+
totalCostUsd: inst.total_cost_usd || inst.totalCostUsd || 0,
|
|
251
|
+
avgLatencyMs: inst.avg_latency_ms || inst.avgLatencyMs || 0,
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
/** Stop and clean up */
|
|
257
|
+
stop() {
|
|
258
|
+
if (this.ws) {
|
|
259
|
+
this.ws.close();
|
|
260
|
+
this.ws = null;
|
|
261
|
+
}
|
|
262
|
+
if (this.reconnectTimer) {
|
|
263
|
+
clearTimeout(this.reconnectTimer);
|
|
264
|
+
this.reconnectTimer = null;
|
|
265
|
+
}
|
|
266
|
+
this.listeners.clear();
|
|
267
|
+
}
|
|
268
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-guardian/cli",
|
|
3
|
+
"version": "4.1.2",
|
|
4
|
+
"description": "CLI binary for scanning MCP tool definitions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-guardian": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@mcp-guardian/core": "^4.1.2",
|
|
12
|
+
"@mcp-guardian/server": "^4.1.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.4.0",
|
|
16
|
+
"vitest": "^3.2.4",
|
|
17
|
+
"@types/node": "^20.0.0"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-guardian/cli",
|
|
3
|
+
"version": "4.1.2",
|
|
4
|
+
"description": "CLI binary for scanning MCP tool definitions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-guardian": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "pnpm --dir ../server run build && tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"prepack": "node ../../scripts/prepack-npm-deps.mjs",
|
|
15
|
+
"postpack": "node ../../scripts/postpack-npm-deps.mjs",
|
|
16
|
+
"prepublishOnly": "pnpm run build && node ../../scripts/validate-npm-pack.mjs"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@mcp-guardian/core": "workspace:^4.1.2",
|
|
20
|
+
"@mcp-guardian/server": "workspace:^4.1.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.4.0",
|
|
24
|
+
"vitest": "^3.2.4",
|
|
25
|
+
"@types/node": "^20.0.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { scanServer, verifyToolDefinitions } from "@mcp-guardian/core";
|
|
7
|
+
import { fetchToolsFromStdio } from "@mcp-guardian/core";
|
|
8
|
+
import { fetchToolsFromHttp, fetchToolsFromSse } from "@mcp-guardian/core";
|
|
9
|
+
import type { ServerScanResult, ToolScanResult } from "@mcp-guardian/core";
|
|
10
|
+
|
|
11
|
+
interface ClaudeDesktopConfig {
|
|
12
|
+
mcpServers?: Record<string, {
|
|
13
|
+
command?: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
url?: string;
|
|
17
|
+
transport?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { values: flags, positionals } = parseArgs({
|
|
22
|
+
args: process.argv.slice(2),
|
|
23
|
+
options: {
|
|
24
|
+
json: { type: "boolean", default: false },
|
|
25
|
+
"fail-on-critical": { type: "boolean", default: false },
|
|
26
|
+
"fail-on-warning": { type: "boolean", default: false },
|
|
27
|
+
"skip-semantic": { type: "boolean", default: false },
|
|
28
|
+
"skip-pinning": { type: "boolean", default: false },
|
|
29
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
30
|
+
url: { type: "string" },
|
|
31
|
+
transport: { type: "string" },
|
|
32
|
+
server: { type: "string" },
|
|
33
|
+
mcp: { type: "boolean", default: false },
|
|
34
|
+
},
|
|
35
|
+
allowPositionals: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function resolveConfigPath(): Promise<string> {
|
|
39
|
+
if (positionals[0]) return positionals[0];
|
|
40
|
+
const candidates = [
|
|
41
|
+
join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
42
|
+
join(homedir(), ".config", "Claude", "claude_desktop_config.json"),
|
|
43
|
+
join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json"),
|
|
44
|
+
];
|
|
45
|
+
return candidates.find(existsSync) ?? candidates[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printReport(results: ServerScanResult[], verbose: boolean): void {
|
|
49
|
+
for (const server of results) {
|
|
50
|
+
const icon = server.status === "critical" ? "[CRIT]" :
|
|
51
|
+
server.status === "warning" ? "[WARN]" : "[OK]";
|
|
52
|
+
|
|
53
|
+
console.log(`\n${icon} ${server.serverName} [${server.transport}]`);
|
|
54
|
+
console.log(` ${server.summary.total} tools | ${server.summary.critical} critical | ${server.summary.warnings} warnings | ${server.summary.clean} clean`);
|
|
55
|
+
|
|
56
|
+
for (const tool of server.tools) {
|
|
57
|
+
if (tool.status === "clean" && !verbose) continue;
|
|
58
|
+
const toolIcon = tool.status === "critical" ? " [CRIT]" : " [WARN]";
|
|
59
|
+
console.log(`${toolIcon} ${tool.toolName}`);
|
|
60
|
+
for (const issue of tool.issues) {
|
|
61
|
+
if (issue.severity === "info") continue;
|
|
62
|
+
console.log(` [${issue.id}] ${issue.message}`);
|
|
63
|
+
if (verbose && issue.evidence) console.log(` evidence: "${issue.evidence}"`);
|
|
64
|
+
if (verbose && issue.confidence < 1.0) console.log(` confidence: ${(issue.confidence * 100).toFixed(0)}%`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
if (flags.mcp) {
|
|
72
|
+
const { startMcpServer } = await import("@mcp-guardian/server");
|
|
73
|
+
await startMcpServer();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const results: ServerScanResult[] = [];
|
|
78
|
+
|
|
79
|
+
// Single URL mode
|
|
80
|
+
if (flags.url) {
|
|
81
|
+
const transport = (flags.transport ?? "http") as "stdio" | "http" | "sse";
|
|
82
|
+
const fetchFn = transport === "sse" ? fetchToolsFromSse : fetchToolsFromHttp;
|
|
83
|
+
const tools = await fetchFn({ url: flags.url });
|
|
84
|
+
const scanResult = await scanServer(flags.url, tools, transport, { skipSemantic: flags["skip-semantic"] });
|
|
85
|
+
|
|
86
|
+
if (!flags["skip-pinning"]) {
|
|
87
|
+
const pinResult = verifyToolDefinitions(tools, flags.url);
|
|
88
|
+
if (pinResult.status === "changed" || pinResult.status === "tampered") {
|
|
89
|
+
scanResult.status = "critical";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
results.push(scanResult);
|
|
93
|
+
} else {
|
|
94
|
+
// Claude Desktop config mode
|
|
95
|
+
const configPath = await resolveConfigPath();
|
|
96
|
+
if (!existsSync(configPath)) {
|
|
97
|
+
console.error(`Config not found: ${configPath}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const config: ClaudeDesktopConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
102
|
+
const servers = config.mcpServers ?? {};
|
|
103
|
+
|
|
104
|
+
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
105
|
+
if (flags.server && serverName !== flags.server) continue;
|
|
106
|
+
|
|
107
|
+
let tools;
|
|
108
|
+
let transport: "stdio" | "http" | "sse" = "stdio";
|
|
109
|
+
|
|
110
|
+
if (serverConfig.url) {
|
|
111
|
+
const raw = (serverConfig.transport as string | undefined) ?? "http";
|
|
112
|
+
if (raw === "sse") {
|
|
113
|
+
transport = "sse";
|
|
114
|
+
tools = await fetchToolsFromSse({ url: serverConfig.url });
|
|
115
|
+
} else {
|
|
116
|
+
transport = "http";
|
|
117
|
+
tools = await fetchToolsFromHttp({ url: serverConfig.url });
|
|
118
|
+
}
|
|
119
|
+
} else if (serverConfig.command) {
|
|
120
|
+
tools = await fetchToolsFromStdio({ command: serverConfig.command, args: serverConfig.args, env: serverConfig.env });
|
|
121
|
+
} else {
|
|
122
|
+
console.warn(` Skipping ${serverName}: no command or URL`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const scanResult = await scanServer(serverName, tools, transport, { skipSemantic: flags["skip-semantic"] });
|
|
127
|
+
|
|
128
|
+
if (!flags["skip-pinning"]) {
|
|
129
|
+
const pinResult = verifyToolDefinitions(tools, serverName);
|
|
130
|
+
if (pinResult.status === "changed") console.warn(` Tool definitions changed in "${serverName}" since last approval`);
|
|
131
|
+
if (pinResult.status === "tampered") {
|
|
132
|
+
console.error(` Manifest tampered for "${serverName}" - treat as critical`);
|
|
133
|
+
scanResult.status = "critical";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
results.push(scanResult);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (flags.json) {
|
|
141
|
+
console.log(JSON.stringify(results, null, 2));
|
|
142
|
+
} else {
|
|
143
|
+
printReport(results, flags.verbose ?? false);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hasCritical = results.some(r => r.status === "critical");
|
|
147
|
+
const hasWarning = results.some(r => r.status === "warning");
|
|
148
|
+
|
|
149
|
+
if (flags["fail-on-critical"] && hasCritical) process.exit(2);
|
|
150
|
+
if (flags["fail-on-warning"] && (hasCritical || hasWarning)) process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch(err => {
|
|
154
|
+
console.error("Fatal:", err.message);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|