@inner-security/mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +65 -0
- package/bundle/index.d.ts +136 -0
- package/bundle/index.js +893 -0
- package/bundle/server.js +883 -0
- package/package.json +70 -0
package/bundle/server.js
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// ../contracts/dist/enums.js
|
|
11
|
+
var ECOSYSTEM = ["npm", "pypi"];
|
|
12
|
+
var STATUS_TO_ACTION = {
|
|
13
|
+
benign: "ALLOW",
|
|
14
|
+
suspicious: "REVIEW",
|
|
15
|
+
malicious: "BLOCK",
|
|
16
|
+
unknown: "PENDING"
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ../contracts/dist/fixtures.js
|
|
20
|
+
var NOW = "2026-06-27T00:00:00Z";
|
|
21
|
+
var WEEK = "2026-07-04T00:00:00Z";
|
|
22
|
+
function v(p) {
|
|
23
|
+
const status = p.status;
|
|
24
|
+
return {
|
|
25
|
+
ecosystem: "npm",
|
|
26
|
+
action: STATUS_TO_ACTION[status],
|
|
27
|
+
confidence: status === "malicious" ? 0.97 : status === "suspicious" ? 0.6 : 0.95,
|
|
28
|
+
behaviors: [],
|
|
29
|
+
corroboration: "behavioral_only",
|
|
30
|
+
evidence_uri: `s3://inner-evidence/${p.name}@${p.version}.json`,
|
|
31
|
+
engine_version: "v0.1.0",
|
|
32
|
+
scanned_at: NOW,
|
|
33
|
+
expires_at: WEEK,
|
|
34
|
+
engine_signature: "ed25519:STUB",
|
|
35
|
+
version_id: `${p.name}@${p.version}`,
|
|
36
|
+
...p
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var SAMPLE_VERDICTS = [
|
|
40
|
+
// --- 10 malicious ---
|
|
41
|
+
// event-stream-style: real-world supply-chain attack (flatmap-stream payload).
|
|
42
|
+
v({
|
|
43
|
+
name: "event-stream",
|
|
44
|
+
version: "3.3.6",
|
|
45
|
+
status: "malicious",
|
|
46
|
+
content_hash: "sha256:bad1",
|
|
47
|
+
corroboration: "corroborated",
|
|
48
|
+
behaviors: [
|
|
49
|
+
{ category: "credential_theft", severity: "critical", evidence_ref: "trace#1" },
|
|
50
|
+
{ category: "c2_communication", severity: "high", evidence_ref: "trace#2" }
|
|
51
|
+
],
|
|
52
|
+
capabilities: [
|
|
53
|
+
{ capability: "reads_credentials", source: "hook" },
|
|
54
|
+
{ capability: "network_exfil", source: "network" }
|
|
55
|
+
]
|
|
56
|
+
}),
|
|
57
|
+
// Typosquat of 'react'.
|
|
58
|
+
v({
|
|
59
|
+
name: "reaact",
|
|
60
|
+
version: "1.0.0",
|
|
61
|
+
status: "malicious",
|
|
62
|
+
content_hash: "sha256:bad2",
|
|
63
|
+
corroboration: "corroborated",
|
|
64
|
+
behaviors: [{ category: "malicious_download", severity: "high", evidence_ref: "trace#1" }],
|
|
65
|
+
capabilities: [{ capability: "self_propagation", source: "static" }]
|
|
66
|
+
}),
|
|
67
|
+
// Behavioral-only: spawns shell during install. Behavioral_only → REVIEW until eval-certified.
|
|
68
|
+
v({
|
|
69
|
+
name: "left-pad-2",
|
|
70
|
+
version: "0.0.1",
|
|
71
|
+
status: "malicious",
|
|
72
|
+
content_hash: "sha256:bad3",
|
|
73
|
+
corroboration: "behavioral_only",
|
|
74
|
+
behaviors: [{ category: "command_execution", severity: "high", evidence_ref: "trace#1" }],
|
|
75
|
+
capabilities: [{ capability: "spawns_process", source: "syscall" }]
|
|
76
|
+
}),
|
|
77
|
+
// Postinstall script reads ~/.aws/credentials and POSTs to an external host.
|
|
78
|
+
v({
|
|
79
|
+
name: "aws-helper-cli",
|
|
80
|
+
version: "0.4.2",
|
|
81
|
+
status: "malicious",
|
|
82
|
+
content_hash: "sha256:bad4",
|
|
83
|
+
corroboration: "corroborated",
|
|
84
|
+
behaviors: [
|
|
85
|
+
{ category: "credential_theft", severity: "critical", evidence_ref: "trace#1" },
|
|
86
|
+
{ category: "data_exfiltration", severity: "critical", evidence_ref: "trace#2" }
|
|
87
|
+
],
|
|
88
|
+
capabilities: [
|
|
89
|
+
{ capability: "reads_credentials", source: "syscall" },
|
|
90
|
+
{ capability: "network_exfil", source: "network" }
|
|
91
|
+
]
|
|
92
|
+
}),
|
|
93
|
+
// Obfuscated eval(atob(...)) payload — dynamic code execution detected statically.
|
|
94
|
+
v({
|
|
95
|
+
name: "kazka-utils",
|
|
96
|
+
version: "2.1.0",
|
|
97
|
+
status: "malicious",
|
|
98
|
+
content_hash: "sha256:bad5",
|
|
99
|
+
corroboration: "behavioral_only",
|
|
100
|
+
behaviors: [{ category: "dynamic_code_execution", severity: "high", evidence_ref: "trace#1" }],
|
|
101
|
+
capabilities: [{ capability: "obfuscated_payload", source: "static" }]
|
|
102
|
+
}),
|
|
103
|
+
// PyPI: setup.py performs persistence write to ~/.bashrc.
|
|
104
|
+
v({
|
|
105
|
+
ecosystem: "pypi",
|
|
106
|
+
name: "colorama-utils",
|
|
107
|
+
version: "0.4.5",
|
|
108
|
+
status: "malicious",
|
|
109
|
+
content_hash: "sha256:bad6",
|
|
110
|
+
corroboration: "behavioral_only",
|
|
111
|
+
behaviors: [{ category: "persistence", severity: "high", evidence_ref: "trace#1" }],
|
|
112
|
+
capabilities: [{ capability: "persistence", source: "syscall" }]
|
|
113
|
+
}),
|
|
114
|
+
// C2 beacon over WebSocket on a regular interval — slow-lane confirmed.
|
|
115
|
+
v({
|
|
116
|
+
name: "discord-bot-helper",
|
|
117
|
+
version: "0.0.7",
|
|
118
|
+
status: "malicious",
|
|
119
|
+
content_hash: "sha256:bad7",
|
|
120
|
+
corroboration: "corroborated",
|
|
121
|
+
behaviors: [{ category: "c2_communication", severity: "critical", evidence_ref: "trace#1" }],
|
|
122
|
+
capabilities: [{ capability: "c2_beacon", source: "network" }]
|
|
123
|
+
}),
|
|
124
|
+
// PyPI typosquat: 'requrest' -> 'requests'. Reads env vars and writes /tmp/.x.
|
|
125
|
+
v({
|
|
126
|
+
ecosystem: "pypi",
|
|
127
|
+
name: "requrest",
|
|
128
|
+
version: "2.31.0",
|
|
129
|
+
status: "malicious",
|
|
130
|
+
content_hash: "sha256:bad8",
|
|
131
|
+
corroboration: "corroborated",
|
|
132
|
+
behaviors: [
|
|
133
|
+
{ category: "data_collection", severity: "medium", evidence_ref: "trace#1" },
|
|
134
|
+
{ category: "file_manipulation", severity: "medium", evidence_ref: "trace#2" }
|
|
135
|
+
],
|
|
136
|
+
capabilities: [
|
|
137
|
+
{ capability: "env_recon", source: "hook" },
|
|
138
|
+
{ capability: "modifies_files", source: "syscall" }
|
|
139
|
+
]
|
|
140
|
+
}),
|
|
141
|
+
// Reverse shell binding /bin/sh to a remote port during import.
|
|
142
|
+
v({
|
|
143
|
+
name: "fastly-cdn-uploader",
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
status: "malicious",
|
|
146
|
+
content_hash: "sha256:bad9",
|
|
147
|
+
corroboration: "corroborated",
|
|
148
|
+
behaviors: [{ category: "reverse_shell", severity: "critical", evidence_ref: "trace#1" }],
|
|
149
|
+
capabilities: [
|
|
150
|
+
{ capability: "spawns_process", source: "syscall" },
|
|
151
|
+
{ capability: "network_exfil", source: "network" }
|
|
152
|
+
]
|
|
153
|
+
}),
|
|
154
|
+
// Web-injection payload: serves modified content on import; common in browser-side attacks.
|
|
155
|
+
v({
|
|
156
|
+
name: "analytics-tracker-pro",
|
|
157
|
+
version: "3.0.0",
|
|
158
|
+
status: "malicious",
|
|
159
|
+
content_hash: "sha256:bad10",
|
|
160
|
+
corroboration: "behavioral_only",
|
|
161
|
+
behaviors: [{ category: "web_injection", severity: "medium", evidence_ref: "trace#1" }]
|
|
162
|
+
}),
|
|
163
|
+
// --- 10 benign ---
|
|
164
|
+
v({ name: "react", version: "18.3.1", status: "benign", content_hash: "sha256:ok1" }),
|
|
165
|
+
v({ name: "lodash", version: "4.17.21", status: "benign", content_hash: "sha256:ok2" }),
|
|
166
|
+
v({ name: "axios", version: "1.7.2", status: "benign", content_hash: "sha256:ok3" }),
|
|
167
|
+
v({ name: "typescript", version: "5.5.4", status: "benign", content_hash: "sha256:ok4" }),
|
|
168
|
+
v({ name: "express", version: "4.19.2", status: "benign", content_hash: "sha256:ok5" }),
|
|
169
|
+
v({ name: "vite", version: "5.4.0", status: "benign", content_hash: "sha256:ok6" }),
|
|
170
|
+
// Heavy native build (node-gyp). Long install time is legitimate, not malicious.
|
|
171
|
+
v({ name: "node-sass", version: "9.0.0", status: "benign", content_hash: "sha256:ok7" }),
|
|
172
|
+
v({ ecosystem: "pypi", name: "requests", version: "2.31.0", status: "benign", content_hash: "sha256:ok8" }),
|
|
173
|
+
v({ ecosystem: "pypi", name: "numpy", version: "1.26.4", status: "benign", content_hash: "sha256:ok9" }),
|
|
174
|
+
v({ ecosystem: "pypi", name: "pandas", version: "2.2.2", status: "benign", content_hash: "sha256:ok10" })
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
// ../verdict-client/dist/client.js
|
|
178
|
+
var DEFAULT_API_URL = "http://localhost:8787";
|
|
179
|
+
function isPending(item) {
|
|
180
|
+
return item.action === "PENDING";
|
|
181
|
+
}
|
|
182
|
+
function isEnvelope(item) {
|
|
183
|
+
return !isPending(item);
|
|
184
|
+
}
|
|
185
|
+
function sleep(ms) {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
187
|
+
}
|
|
188
|
+
function stripTrailingSlash(u) {
|
|
189
|
+
return u.replace(/\/+$/, "");
|
|
190
|
+
}
|
|
191
|
+
var VerdictClient = class {
|
|
192
|
+
baseUrl;
|
|
193
|
+
apiKey;
|
|
194
|
+
timeoutMs;
|
|
195
|
+
maxPollAttempts;
|
|
196
|
+
maxRetries;
|
|
197
|
+
fetchImpl;
|
|
198
|
+
cache;
|
|
199
|
+
constructor(opts = {}) {
|
|
200
|
+
this.baseUrl = stripTrailingSlash(opts.baseUrl ?? process.env.INNER_API_URL ?? DEFAULT_API_URL);
|
|
201
|
+
this.apiKey = opts.apiKey ?? process.env.INNER_API_KEY;
|
|
202
|
+
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
203
|
+
this.maxPollAttempts = opts.maxPollAttempts ?? 8;
|
|
204
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
205
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
206
|
+
this.cache = opts.cache;
|
|
207
|
+
if (typeof this.fetchImpl !== "function") {
|
|
208
|
+
throw new Error("No fetch implementation available (Node >= 18 required, or pass fetchImpl).");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
headers() {
|
|
212
|
+
const h = { "content-type": "application/json" };
|
|
213
|
+
if (this.apiKey)
|
|
214
|
+
h.authorization = `Bearer ${this.apiKey}`;
|
|
215
|
+
return h;
|
|
216
|
+
}
|
|
217
|
+
/** A single fetch with a per-request AbortController timeout. */
|
|
218
|
+
async fetchOnce(url, init) {
|
|
219
|
+
const ctrl = new AbortController();
|
|
220
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
221
|
+
try {
|
|
222
|
+
return await this.fetchImpl(url, { ...init, signal: ctrl.signal });
|
|
223
|
+
} finally {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* fetch with timeout + retries-with-backoff on transient failures (network
|
|
229
|
+
* error / timeout / 5xx). 404 is returned as-is (the poll path treats it as
|
|
230
|
+
* "not ready yet"); other 4xx are returned as-is for the caller to handle.
|
|
231
|
+
*/
|
|
232
|
+
async fetchWithRetry(url, init) {
|
|
233
|
+
let lastErr;
|
|
234
|
+
let wait = 250;
|
|
235
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
236
|
+
try {
|
|
237
|
+
const res = await this.fetchOnce(url, init);
|
|
238
|
+
if (res.status >= 500 && attempt < this.maxRetries) {
|
|
239
|
+
await sleep(wait);
|
|
240
|
+
wait = Math.min(wait * 2, 5e3);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
return res;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
lastErr = err;
|
|
246
|
+
if (attempt < this.maxRetries) {
|
|
247
|
+
await sleep(wait);
|
|
248
|
+
wait = Math.min(wait * 2, 5e3);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw lastErr instanceof Error ? lastErr : new Error(`Request to ${url} failed`);
|
|
254
|
+
}
|
|
255
|
+
/** Raw C1 batch call. Items may come back as VerdictEnvelope or PendingResponse. */
|
|
256
|
+
async getVerdicts(items) {
|
|
257
|
+
const body = { items };
|
|
258
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}/v1/verdict`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: this.headers(),
|
|
261
|
+
body: JSON.stringify(body)
|
|
262
|
+
});
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
throw new Error(`Verdict API POST /v1/verdict failed: ${res.status} ${res.statusText}`);
|
|
265
|
+
}
|
|
266
|
+
return await res.json();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Poll a PENDING job to resolution. Returns the resolved item (may still be
|
|
270
|
+
* PENDING if it never resolves within maxPollAttempts). 404 is treated as
|
|
271
|
+
* "not ready yet" and retried; a non-ok, non-404 response throws.
|
|
272
|
+
*/
|
|
273
|
+
async pollVerdict(pending) {
|
|
274
|
+
let attempt = 0;
|
|
275
|
+
let waitMs = Math.max(250, pending.eta_ms || 1e3);
|
|
276
|
+
let last = pending;
|
|
277
|
+
const url = `${this.baseUrl}/v1/verdict/${encodeURIComponent(pending.job_id)}`;
|
|
278
|
+
while (attempt < this.maxPollAttempts) {
|
|
279
|
+
await sleep(Math.min(waitMs, 5e3));
|
|
280
|
+
const res = await this.fetchWithRetry(url, {
|
|
281
|
+
method: "GET",
|
|
282
|
+
headers: this.headers()
|
|
283
|
+
});
|
|
284
|
+
if (res.status === 404) {
|
|
285
|
+
attempt += 1;
|
|
286
|
+
waitMs = Math.min(waitMs * 2, 5e3);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (!res.ok) {
|
|
290
|
+
throw new Error(`Verdict API GET /v1/verdict/${pending.job_id} failed: ${res.status} ${res.statusText}`);
|
|
291
|
+
}
|
|
292
|
+
last = await res.json();
|
|
293
|
+
if (!isPending(last))
|
|
294
|
+
return last;
|
|
295
|
+
attempt += 1;
|
|
296
|
+
waitMs = Math.min(waitMs * 2, 5e3);
|
|
297
|
+
}
|
|
298
|
+
return last;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Resolve a single item to a final envelope, polling through PENDING. Returns
|
|
302
|
+
* the envelope, or the unresolved PendingResponse if it never landed.
|
|
303
|
+
*/
|
|
304
|
+
async resolveVerdict(item) {
|
|
305
|
+
const { results } = await this.getVerdicts([item]);
|
|
306
|
+
const first = results[0];
|
|
307
|
+
if (!first)
|
|
308
|
+
throw new Error("Verdict API returned no results for the request.");
|
|
309
|
+
if (isPending(first))
|
|
310
|
+
return this.pollVerdict(first);
|
|
311
|
+
return first;
|
|
312
|
+
}
|
|
313
|
+
/** Resolve a batch, polling each PENDING to resolution. Order matches input. */
|
|
314
|
+
async resolveVerdicts(items) {
|
|
315
|
+
const { results } = await this.getVerdicts(items);
|
|
316
|
+
return Promise.all(results.map((r) => isPending(r) ? this.pollVerdict(r) : Promise.resolve(r)));
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* High-level fail-open resolution (the CLI scan path). Returns one
|
|
320
|
+
* ResolvedItem per requested item. On a total API failure (network/timeout),
|
|
321
|
+
* falls back to the fail-open cache for every item; for individual
|
|
322
|
+
* missing/never-resolved items, falls back to the cache too. Items the cache
|
|
323
|
+
* cannot safely satisfy surface as 'unresolved' (UNKNOWN) so the operator
|
|
324
|
+
* decides — never a silent allow.
|
|
325
|
+
*/
|
|
326
|
+
async resolve(items) {
|
|
327
|
+
if (items.length === 0)
|
|
328
|
+
return { resolved: [], degraded: false };
|
|
329
|
+
let response;
|
|
330
|
+
try {
|
|
331
|
+
response = await this.getVerdicts(items);
|
|
332
|
+
} catch {
|
|
333
|
+
return this.failOpen(items);
|
|
334
|
+
}
|
|
335
|
+
const results = response.results ?? [];
|
|
336
|
+
const resolved = [];
|
|
337
|
+
for (let i = 0; i < items.length; i++) {
|
|
338
|
+
const item = items[i];
|
|
339
|
+
const r = results[i];
|
|
340
|
+
if (!r) {
|
|
341
|
+
resolved.push(this.fromCacheOrUnresolved(item));
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (isPending(r)) {
|
|
345
|
+
let env;
|
|
346
|
+
try {
|
|
347
|
+
env = await this.pollVerdict(r);
|
|
348
|
+
} catch {
|
|
349
|
+
env = void 0;
|
|
350
|
+
}
|
|
351
|
+
if (env && isEnvelope(env)) {
|
|
352
|
+
this.cache?.put(item, env);
|
|
353
|
+
resolved.push({ item, envelope: env, source: "api" });
|
|
354
|
+
} else {
|
|
355
|
+
resolved.push(this.fromCacheOrUnresolved(item));
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
this.cache?.put(item, r);
|
|
360
|
+
resolved.push({ item, envelope: r, source: "api" });
|
|
361
|
+
}
|
|
362
|
+
return { resolved, degraded: false };
|
|
363
|
+
}
|
|
364
|
+
failOpen(items) {
|
|
365
|
+
const resolved = items.map((item) => {
|
|
366
|
+
const cached = this.cache?.get(item);
|
|
367
|
+
if (cached) {
|
|
368
|
+
return { item, envelope: cached, source: "cache", failedOpen: true };
|
|
369
|
+
}
|
|
370
|
+
return { item, source: "unresolved", failedOpen: true };
|
|
371
|
+
});
|
|
372
|
+
return { resolved, degraded: true };
|
|
373
|
+
}
|
|
374
|
+
fromCacheOrUnresolved(item) {
|
|
375
|
+
const cached = this.cache?.get(item);
|
|
376
|
+
if (cached)
|
|
377
|
+
return { item, envelope: cached, source: "cache" };
|
|
378
|
+
return { item, source: "unresolved" };
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/handlers.ts
|
|
383
|
+
import { randomUUID } from "crypto";
|
|
384
|
+
|
|
385
|
+
// src/alternatives.ts
|
|
386
|
+
var CURATED = {
|
|
387
|
+
// npm typosquats / known-bad fixtures -> the legitimate package.
|
|
388
|
+
"npm:reaact": { suggestion: "react", rationale: 'Likely typosquat of the official "react" package.' },
|
|
389
|
+
"npm:event-stream": {
|
|
390
|
+
suggestion: "readable-stream",
|
|
391
|
+
rationale: 'event-stream@3.3.6 carried the flatmap-stream supply-chain payload; "readable-stream" is the maintained, widely-used stream toolkit.'
|
|
392
|
+
},
|
|
393
|
+
"npm:left-pad-2": {
|
|
394
|
+
suggestion: "just-pad-left",
|
|
395
|
+
rationale: "Use a maintained, single-purpose padding helper instead of an unknown clone."
|
|
396
|
+
},
|
|
397
|
+
"npm:aws-helper-cli": {
|
|
398
|
+
suggestion: "@aws-sdk/client-sts",
|
|
399
|
+
rationale: "Use the official AWS SDK rather than a third-party credential-touching helper."
|
|
400
|
+
},
|
|
401
|
+
"npm:discord-bot-helper": {
|
|
402
|
+
suggestion: "discord.js",
|
|
403
|
+
rationale: "discord.js is the maintained, audited Discord client library."
|
|
404
|
+
},
|
|
405
|
+
"npm:fastly-cdn-uploader": {
|
|
406
|
+
suggestion: "@fastly/js-compute",
|
|
407
|
+
rationale: "Prefer Fastly's official tooling over an unaffiliated uploader."
|
|
408
|
+
},
|
|
409
|
+
"npm:analytics-tracker-pro": {
|
|
410
|
+
suggestion: "@vercel/analytics",
|
|
411
|
+
rationale: "Use a reputable analytics library instead of an unknown tracker."
|
|
412
|
+
},
|
|
413
|
+
// pypi typosquats / known-bad fixtures.
|
|
414
|
+
"pypi:requrest": {
|
|
415
|
+
suggestion: "requests",
|
|
416
|
+
rationale: 'Likely typosquat of the official "requests" HTTP library.'
|
|
417
|
+
},
|
|
418
|
+
"pypi:colorama-utils": {
|
|
419
|
+
suggestion: "colorama",
|
|
420
|
+
rationale: 'Use the official "colorama" package; the "-utils" variant performs persistence writes.'
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
function suggestAlternative(ecosystem, name) {
|
|
424
|
+
const key = `${ecosystem}:${name.toLowerCase()}`;
|
|
425
|
+
const hit = CURATED[key];
|
|
426
|
+
if (hit) {
|
|
427
|
+
return {
|
|
428
|
+
ecosystem,
|
|
429
|
+
requested: name,
|
|
430
|
+
suggestion: hit.suggestion,
|
|
431
|
+
rationale: hit.rationale,
|
|
432
|
+
source: "curated"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ecosystem,
|
|
437
|
+
requested: name,
|
|
438
|
+
suggestion: null,
|
|
439
|
+
rationale: "No curated safe alternative is known for this package. (suggest_alternative currently uses a small curated map; a real recommendation backend is future work.)",
|
|
440
|
+
source: "none"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/parse-deps.ts
|
|
445
|
+
function cleanNpmVersion(spec) {
|
|
446
|
+
const trimmed = spec.trim();
|
|
447
|
+
if (!trimmed) return null;
|
|
448
|
+
if (trimmed === "*" || trimmed === "latest" || trimmed === "x" || trimmed === "X") return null;
|
|
449
|
+
if (/^(workspace:|link:|file:|git\+|git:|github:|https?:)/.test(trimmed)) return null;
|
|
450
|
+
const m = trimmed.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
451
|
+
if (m) return m[0];
|
|
452
|
+
const stripped = trimmed.replace(/^[\^~>=<\s]+/, "").trim();
|
|
453
|
+
return /^\d/.test(stripped) ? stripped : null;
|
|
454
|
+
}
|
|
455
|
+
function parsePackageJson(content) {
|
|
456
|
+
const json = JSON.parse(content);
|
|
457
|
+
const sections = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
458
|
+
const items = [];
|
|
459
|
+
const warnings = [];
|
|
460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
461
|
+
for (const section of sections) {
|
|
462
|
+
const deps = json[section];
|
|
463
|
+
if (deps && typeof deps === "object") {
|
|
464
|
+
for (const [name, rawSpec] of Object.entries(deps)) {
|
|
465
|
+
if (seen.has(name)) continue;
|
|
466
|
+
seen.add(name);
|
|
467
|
+
const spec = String(rawSpec);
|
|
468
|
+
const alias = spec.match(/^npm:(@?[^@]+(?:\/[^@]+)?)@(.+)$/);
|
|
469
|
+
if (alias) {
|
|
470
|
+
const aliasVersion = cleanNpmVersion(alias[2]);
|
|
471
|
+
if (aliasVersion) {
|
|
472
|
+
items.push({ ecosystem: "npm", name: alias[1], version: aliasVersion });
|
|
473
|
+
} else {
|
|
474
|
+
warnings.push(`Skipped "${name}" (${spec}) \u2014 alias has no concrete version.`);
|
|
475
|
+
}
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const version = cleanNpmVersion(spec);
|
|
479
|
+
if (version) {
|
|
480
|
+
items.push({ ecosystem: "npm", name, version });
|
|
481
|
+
} else {
|
|
482
|
+
warnings.push(`Skipped "${name}" (${spec}) \u2014 no concrete version (wildcard/tag/git/url).`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return { items, warnings, skipped: warnings.length };
|
|
488
|
+
}
|
|
489
|
+
function parsePackageLock(content) {
|
|
490
|
+
const json = JSON.parse(content);
|
|
491
|
+
const items = [];
|
|
492
|
+
const warnings = [];
|
|
493
|
+
const seen = /* @__PURE__ */ new Set();
|
|
494
|
+
for (const [path, meta] of Object.entries(json.packages ?? {})) {
|
|
495
|
+
if (!path || !path.startsWith("node_modules/")) continue;
|
|
496
|
+
const name = path.slice(path.lastIndexOf("node_modules/") + "node_modules/".length);
|
|
497
|
+
if (!name || seen.has(name)) continue;
|
|
498
|
+
seen.add(name);
|
|
499
|
+
if (!meta?.version) {
|
|
500
|
+
warnings.push(`Skipped "${name}" \u2014 no resolved version in lockfile.`);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
items.push({ ecosystem: "npm", name, version: meta.version });
|
|
504
|
+
}
|
|
505
|
+
return { items, warnings, skipped: warnings.length };
|
|
506
|
+
}
|
|
507
|
+
function parseRequirementsTxt(content) {
|
|
508
|
+
const items = [];
|
|
509
|
+
const warnings = [];
|
|
510
|
+
const seen = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const rawLine of content.split("\n")) {
|
|
512
|
+
const line = rawLine.split("#")[0].trim();
|
|
513
|
+
if (!line) continue;
|
|
514
|
+
if (line.startsWith("-") || line.includes("://")) {
|
|
515
|
+
warnings.push(`Skipped "${line.slice(0, 60)}" \u2014 not a simple pin.`);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const m = line.match(/^([A-Za-z0-9._-]+)\s*(?:\[[^\]]*\])?\s*(?:[=<>!~]=?\s*([0-9][\w.+!-]*))?/);
|
|
519
|
+
if (!m) {
|
|
520
|
+
warnings.push(`Skipped "${line.slice(0, 60)}" \u2014 unparseable requirement.`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const name = m[1];
|
|
524
|
+
const version = m[2];
|
|
525
|
+
if (!version) {
|
|
526
|
+
warnings.push(`Skipped "${line.slice(0, 60)}" \u2014 only pinned (pkg==version) deps are resolved.`);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (seen.has(name)) continue;
|
|
530
|
+
seen.add(name);
|
|
531
|
+
items.push({ ecosystem: "pypi", name, version });
|
|
532
|
+
}
|
|
533
|
+
return { items, warnings, skipped: warnings.length };
|
|
534
|
+
}
|
|
535
|
+
function inferFormat(content) {
|
|
536
|
+
const trimmed = content.trim();
|
|
537
|
+
if (trimmed.startsWith("{")) {
|
|
538
|
+
try {
|
|
539
|
+
const json = JSON.parse(trimmed);
|
|
540
|
+
if (json.lockfileVersion !== void 0 || json.packages !== void 0) {
|
|
541
|
+
return "package-lock.json";
|
|
542
|
+
}
|
|
543
|
+
return "package.json";
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return "requirements.txt";
|
|
548
|
+
}
|
|
549
|
+
function parseDependenciesWithWarnings(input) {
|
|
550
|
+
if (input.packages && input.packages.length > 0) {
|
|
551
|
+
return { items: input.packages, warnings: [], skipped: 0 };
|
|
552
|
+
}
|
|
553
|
+
if (!input.content || input.content.trim() === "") {
|
|
554
|
+
throw new Error("check_dependencies needs either `packages` or non-empty `content`.");
|
|
555
|
+
}
|
|
556
|
+
const format = input.format ?? inferFormat(input.content);
|
|
557
|
+
switch (format) {
|
|
558
|
+
case "package.json":
|
|
559
|
+
return parsePackageJson(input.content);
|
|
560
|
+
case "package-lock.json":
|
|
561
|
+
return parsePackageLock(input.content);
|
|
562
|
+
case "requirements.txt":
|
|
563
|
+
return parseRequirementsTxt(input.content);
|
|
564
|
+
default:
|
|
565
|
+
throw new Error(`Unsupported manifest format: ${String(format)}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/handlers.ts
|
|
570
|
+
function isBlocked(action) {
|
|
571
|
+
return action === "BLOCK" || action === "REVIEW";
|
|
572
|
+
}
|
|
573
|
+
function stripControlChars(s) {
|
|
574
|
+
return s.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ").replace(/\s+/g, " ").trim();
|
|
575
|
+
}
|
|
576
|
+
function fence(raw, max = 200) {
|
|
577
|
+
const s = stripControlChars(String(raw ?? "")).slice(0, max);
|
|
578
|
+
return `\xAB${s}\xBB`;
|
|
579
|
+
}
|
|
580
|
+
function fenceList(items) {
|
|
581
|
+
return items.map((c) => fence(c, 60)).join(", ");
|
|
582
|
+
}
|
|
583
|
+
function envelopeToVerdict(item, resolved, withAlternative) {
|
|
584
|
+
if (isPending(resolved)) {
|
|
585
|
+
return {
|
|
586
|
+
ecosystem: item.ecosystem,
|
|
587
|
+
name: item.name,
|
|
588
|
+
version: item.version,
|
|
589
|
+
pending: true,
|
|
590
|
+
action: "PENDING",
|
|
591
|
+
summary: `${fence(item.name)}@${fence(item.version, 60)}: verdict PENDING (job ${resolved.job_id}); not resolved yet \u2014 treat as unknown and re-check before install.`
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const env = resolved;
|
|
595
|
+
const blocked = isBlocked(env.action);
|
|
596
|
+
const out = {
|
|
597
|
+
ecosystem: env.ecosystem,
|
|
598
|
+
name: env.name,
|
|
599
|
+
version: env.version,
|
|
600
|
+
pending: false,
|
|
601
|
+
status: env.status,
|
|
602
|
+
action: env.action,
|
|
603
|
+
confidence: env.confidence,
|
|
604
|
+
behaviors: env.behaviors,
|
|
605
|
+
policy_decision: env.policy_decision,
|
|
606
|
+
provisional: env.provisional,
|
|
607
|
+
summary: `${fence(env.name)}@${fence(env.version, 60)}: ${env.status.toUpperCase()} -> ${env.action}${env.behaviors.length ? ` (${fenceList(env.behaviors.map((b) => b.category))})` : ""}${env.provisional ? " [provisional]" : ""}`
|
|
608
|
+
};
|
|
609
|
+
if (withAlternative && blocked) {
|
|
610
|
+
out.suggested_alternative = suggestAlternative(env.ecosystem, env.name);
|
|
611
|
+
}
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
async function checkPackage(client, args) {
|
|
615
|
+
const item = {
|
|
616
|
+
ecosystem: args.ecosystem,
|
|
617
|
+
name: args.name,
|
|
618
|
+
version: args.version
|
|
619
|
+
};
|
|
620
|
+
const resolved = await client.resolveVerdict(item);
|
|
621
|
+
return envelopeToVerdict(item, resolved, true);
|
|
622
|
+
}
|
|
623
|
+
async function checkDependencies(client, args) {
|
|
624
|
+
const { items, warnings, skipped } = parseDependenciesWithWarnings(args);
|
|
625
|
+
if (items.length === 0) {
|
|
626
|
+
return {
|
|
627
|
+
tally: { total: 0, allow: 0, review: 0, block: 0, pending: 0 },
|
|
628
|
+
verdicts: [],
|
|
629
|
+
has_risk: false,
|
|
630
|
+
skipped,
|
|
631
|
+
warnings
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const resolved = await client.resolveVerdicts(items);
|
|
635
|
+
const verdicts = items.map((item, i) => envelopeToVerdict(item, resolved[i], true));
|
|
636
|
+
const tally = {
|
|
637
|
+
total: verdicts.length,
|
|
638
|
+
allow: verdicts.filter((v2) => v2.action === "ALLOW").length,
|
|
639
|
+
review: verdicts.filter((v2) => v2.action === "REVIEW").length,
|
|
640
|
+
block: verdicts.filter((v2) => v2.action === "BLOCK").length,
|
|
641
|
+
pending: verdicts.filter((v2) => v2.action === "PENDING").length
|
|
642
|
+
};
|
|
643
|
+
const has_risk = tally.block + tally.review + tally.pending + skipped > 0;
|
|
644
|
+
return { tally, verdicts, has_risk, skipped, warnings };
|
|
645
|
+
}
|
|
646
|
+
async function explainVerdict(client, args) {
|
|
647
|
+
const item = {
|
|
648
|
+
ecosystem: args.ecosystem,
|
|
649
|
+
name: args.name,
|
|
650
|
+
version: args.version
|
|
651
|
+
};
|
|
652
|
+
const resolved = await client.resolveVerdict(item);
|
|
653
|
+
if (isPending(resolved)) {
|
|
654
|
+
return {
|
|
655
|
+
ecosystem: args.ecosystem,
|
|
656
|
+
name: args.name,
|
|
657
|
+
version: args.version,
|
|
658
|
+
pending: true,
|
|
659
|
+
explanation: `No verdict is available yet for ${fence(args.name)}@${fence(args.version, 60)} (still PENDING, job ${resolved.job_id}). The package is being analyzed; re-check shortly.`
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const env = resolved;
|
|
663
|
+
const behaviorLines = env.behaviors.map(
|
|
664
|
+
(b) => `- ${fence(b.category, 60)} (${fence(b.severity, 40)}) [evidence: ${fence(b.evidence_ref, 120)}]`
|
|
665
|
+
);
|
|
666
|
+
const explanation = `${fence(env.name)}@${fence(env.version, 60)} is ${env.status.toUpperCase()} -> action ${env.action} (confidence ${(env.confidence * 100).toFixed(0)}%, ${env.corroboration}${env.provisional ? ", provisional" : ""}).
|
|
667
|
+
` + (behaviorLines.length ? `Observed behaviors:
|
|
668
|
+
${behaviorLines.join("\n")}` : "No specific behaviors were flagged.") + (env.reasoning ? `
|
|
669
|
+
Engine reasoning: ${fence(env.reasoning, 500)}` : "") + `
|
|
670
|
+
Evidence trace: ${fence(env.evidence_uri, 200)}`;
|
|
671
|
+
return {
|
|
672
|
+
ecosystem: env.ecosystem,
|
|
673
|
+
name: env.name,
|
|
674
|
+
version: env.version,
|
|
675
|
+
pending: false,
|
|
676
|
+
status: env.status,
|
|
677
|
+
action: env.action,
|
|
678
|
+
confidence: env.confidence,
|
|
679
|
+
corroboration: env.corroboration,
|
|
680
|
+
assurance: env.assurance,
|
|
681
|
+
provisional: env.provisional,
|
|
682
|
+
behaviors: env.behaviors.map((b) => ({
|
|
683
|
+
category: b.category,
|
|
684
|
+
severity: b.severity,
|
|
685
|
+
evidence_ref: b.evidence_ref
|
|
686
|
+
})),
|
|
687
|
+
capabilities: env.capabilities,
|
|
688
|
+
reasoning: env.reasoning,
|
|
689
|
+
evidence_uri: env.evidence_uri,
|
|
690
|
+
explanation
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function suggestAlternativeHandler(args) {
|
|
694
|
+
return suggestAlternative(args.ecosystem, args.name);
|
|
695
|
+
}
|
|
696
|
+
var ReviewStore = class {
|
|
697
|
+
reviews = [];
|
|
698
|
+
record(args) {
|
|
699
|
+
const review = {
|
|
700
|
+
// crypto.randomUUID() avoids the ms-granularity collisions of Date.now()
|
|
701
|
+
// when two reviews for the same coordinate are recorded in the same tick.
|
|
702
|
+
id: `review-${args.ecosystem}-${args.name}${args.version ? `@${args.version}` : ""}-${randomUUID()}`,
|
|
703
|
+
ecosystem: args.ecosystem,
|
|
704
|
+
name: args.name,
|
|
705
|
+
version: args.version,
|
|
706
|
+
reason: args.reason,
|
|
707
|
+
requested_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
708
|
+
};
|
|
709
|
+
this.reviews.push(review);
|
|
710
|
+
return review;
|
|
711
|
+
}
|
|
712
|
+
list() {
|
|
713
|
+
return [...this.reviews];
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// src/server.ts
|
|
718
|
+
var ecosystemSchema = z.enum(ECOSYSTEM);
|
|
719
|
+
function ok(structured, text) {
|
|
720
|
+
return {
|
|
721
|
+
content: [{ type: "text", text }],
|
|
722
|
+
structuredContent: structured
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function fail(err) {
|
|
726
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
727
|
+
return {
|
|
728
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
729
|
+
isError: true
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function buildServer(clientOpts = {}) {
|
|
733
|
+
const client = new VerdictClient(clientOpts);
|
|
734
|
+
const reviews = new ReviewStore();
|
|
735
|
+
const server = new McpServer(
|
|
736
|
+
{ name: "inner-mcp", version: "0.1.0" },
|
|
737
|
+
{
|
|
738
|
+
instructions: "Inner security gate for package installs. Before you run npm/pip install, call check_package (or check_dependencies for a whole manifest). If a package is BLOCK or REVIEW, do NOT install it \u2014 use suggest_alternative for a safer option, or request_review to flag it. Use explain_verdict to show the user why."
|
|
739
|
+
}
|
|
740
|
+
);
|
|
741
|
+
server.registerTool(
|
|
742
|
+
"check_package",
|
|
743
|
+
{
|
|
744
|
+
title: "Check a package before installing",
|
|
745
|
+
description: "Get the Inner verdict (status/action/confidence/behaviors) for a single package, plus a suggested safer alternative when it is blocked. Call this BEFORE installing.",
|
|
746
|
+
inputSchema: {
|
|
747
|
+
ecosystem: ecosystemSchema.describe("Package ecosystem: npm or pypi"),
|
|
748
|
+
name: z.string().describe('Package name, e.g. "react"'),
|
|
749
|
+
version: z.string().describe('Exact version, e.g. "18.3.1"')
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
async (args) => {
|
|
753
|
+
try {
|
|
754
|
+
const verdict = await checkPackage(client, args);
|
|
755
|
+
return ok(verdict, verdict.summary);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return fail(err);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
server.registerTool(
|
|
762
|
+
"check_dependencies",
|
|
763
|
+
{
|
|
764
|
+
title: "Check a whole dependency set",
|
|
765
|
+
description: "Resolve verdicts for an entire manifest/lockfile (package.json, package-lock.json, requirements.txt) or an explicit package list. Returns per-package verdicts + a tally.",
|
|
766
|
+
inputSchema: {
|
|
767
|
+
content: z.string().optional().describe("Raw manifest/lockfile contents (package.json, package-lock.json, or requirements.txt)."),
|
|
768
|
+
format: z.enum(["package.json", "package-lock.json", "requirements.txt"]).optional().describe("Parser to use; inferred from content when omitted."),
|
|
769
|
+
ecosystem: ecosystemSchema.optional().describe("Ecosystem hint for ambiguous inputs."),
|
|
770
|
+
packages: z.array(
|
|
771
|
+
z.object({
|
|
772
|
+
ecosystem: ecosystemSchema,
|
|
773
|
+
name: z.string(),
|
|
774
|
+
version: z.string()
|
|
775
|
+
})
|
|
776
|
+
).optional().describe("Explicit package list (bypasses parsing).")
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
async (args) => {
|
|
780
|
+
try {
|
|
781
|
+
const result = await checkDependencies(client, args);
|
|
782
|
+
const { tally } = result;
|
|
783
|
+
const text = `Checked ${tally.total} package(s): ${tally.allow} ALLOW, ${tally.review} REVIEW, ${tally.block} BLOCK, ${tally.pending} PENDING.` + (result.skipped > 0 ? ` ${result.skipped} entr${result.skipped === 1 ? "y" : "ies"} skipped as unparseable (NOT scanned \u2014 treat as unprotected).` : "") + (result.has_risk ? " Risk present \u2014 review BLOCK/REVIEW/PENDING/skipped before installing." : " All clear.");
|
|
784
|
+
return ok(result, text);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
return fail(err);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
server.registerTool(
|
|
791
|
+
"explain_verdict",
|
|
792
|
+
{
|
|
793
|
+
title: "Explain a verdict",
|
|
794
|
+
description: "Deep-insight reasoning for a package: the behaviors observed, their severity, the evidence each cites, corroboration/assurance, and a narrative explanation.",
|
|
795
|
+
inputSchema: {
|
|
796
|
+
ecosystem: ecosystemSchema.describe("Package ecosystem: npm or pypi"),
|
|
797
|
+
name: z.string().describe("Package name"),
|
|
798
|
+
version: z.string().describe("Exact version")
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
async (args) => {
|
|
802
|
+
try {
|
|
803
|
+
const explanation = await explainVerdict(client, args);
|
|
804
|
+
return ok(explanation, explanation.explanation);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
return fail(err);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
server.registerTool(
|
|
811
|
+
"suggest_alternative",
|
|
812
|
+
{
|
|
813
|
+
title: "Suggest a safer alternative",
|
|
814
|
+
description: "For a blocked/risky package, suggest a safer equivalent. Heuristic/curated for now (a real recommendation backend is future work); returns null when nothing is known.",
|
|
815
|
+
inputSchema: {
|
|
816
|
+
ecosystem: ecosystemSchema.describe("Package ecosystem: npm or pypi"),
|
|
817
|
+
name: z.string().describe("The package to find an alternative for")
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
async (args) => {
|
|
821
|
+
try {
|
|
822
|
+
const suggestion = suggestAlternativeHandler(args);
|
|
823
|
+
const text = suggestion.suggestion ? `Suggested alternative for ${suggestion.requested}: ${suggestion.suggestion} \u2014 ${suggestion.rationale}` : suggestion.rationale;
|
|
824
|
+
return ok(suggestion, text);
|
|
825
|
+
} catch (err) {
|
|
826
|
+
return fail(err);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
server.registerTool(
|
|
831
|
+
"request_review",
|
|
832
|
+
{
|
|
833
|
+
title: "Request a human review",
|
|
834
|
+
description: "Record a request for a human to review a package (e.g. a false positive or an unresolved verdict). Returns a review ticket id.",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
ecosystem: ecosystemSchema.describe("Package ecosystem: npm or pypi"),
|
|
837
|
+
name: z.string().describe("Package name"),
|
|
838
|
+
version: z.string().optional().describe("Version, if known"),
|
|
839
|
+
reason: z.string().optional().describe("Why a review is requested")
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
async (args) => {
|
|
843
|
+
try {
|
|
844
|
+
const review = reviews.record(args);
|
|
845
|
+
return ok(
|
|
846
|
+
review,
|
|
847
|
+
`Review requested for ${review.name}${review.version ? `@${review.version}` : ""} (ticket ${review.id}).`
|
|
848
|
+
);
|
|
849
|
+
} catch (err) {
|
|
850
|
+
return fail(err);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
return server;
|
|
855
|
+
}
|
|
856
|
+
async function main() {
|
|
857
|
+
const server = buildServer();
|
|
858
|
+
const transport = new StdioServerTransport();
|
|
859
|
+
await server.connect(transport);
|
|
860
|
+
const apiUrl = process.env.INNER_API_URL ?? `${DEFAULT_API_URL} (local mock)`;
|
|
861
|
+
process.stderr.write(`[inner-mcp] ready on stdio; Verdict API: ${apiUrl}
|
|
862
|
+
`);
|
|
863
|
+
}
|
|
864
|
+
var invokedDirectly = (() => {
|
|
865
|
+
const entry = process.argv[1];
|
|
866
|
+
if (entry === void 0) return false;
|
|
867
|
+
try {
|
|
868
|
+
const real = realpathSync(entry);
|
|
869
|
+
return import.meta.url === pathToFileURL(real).href;
|
|
870
|
+
} catch {
|
|
871
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
872
|
+
}
|
|
873
|
+
})();
|
|
874
|
+
if (invokedDirectly) {
|
|
875
|
+
main().catch((err) => {
|
|
876
|
+
process.stderr.write(`[inner-mcp] fatal: ${err instanceof Error ? err.stack : String(err)}
|
|
877
|
+
`);
|
|
878
|
+
process.exit(1);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
export {
|
|
882
|
+
buildServer
|
|
883
|
+
};
|