@mmerterden/multi-agent-toolkit-mcp 3.13.1 → 3.14.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/CHANGELOG.md CHANGED
@@ -1320,6 +1320,19 @@ paths and requires an error on each. Gate 9 requires every tool advertising an
1320
1320
  outputSchema to actually return `structuredContent`, and to keep its text
1321
1321
  content beside it. 14 gates, all green.
1322
1322
 
1323
+ ## [3.14.0] - 2026-09-21
1324
+
1325
+ ### Added
1326
+
1327
+ - **`security` family: `security_cvss_score` and `security_dep_inventory`.**
1328
+ security_cvss_score turns a CVSS 3.1 base vector into a score and band by the
1329
+ FIRST arithmetic, so a finding's number cannot drift from its vector.
1330
+ security_dep_inventory normalizes a lockfile (npm, yarn, pnpm, CocoaPods, pip,
1331
+ Go, Cargo, RubyGems) into {ecosystem, name, version}. Both are pure JS and
1332
+ contact nothing - matching an inventory against known CVEs stays a separate,
1333
+ network-bearing step, so the stdio-only egress surface is unchanged. Gated at
1334
+ serve time by `MCP_TOOLKIT_CAPS=security`. 117 tools total.
1335
+
1323
1336
  ## [3.0.0] - 2026-08-22
1324
1337
 
1325
1338
  ### Changed
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  🇹🇷 Türkçe: [README.tr.md](./README.tr.md)
9
9
 
10
- **115 tools** for iOS Simulator, Android Emulator, and headless web control. MCP server that lets your AI coding assistant see, interact with, and audit your mobile apps - plus drive browsers, run an 18-rule App Store compliance audit, and orchestrate multi-step batch flows.
10
+ **117 tools** for iOS Simulator, Android Emulator, and headless web control. MCP server that lets your AI coding assistant see, interact with, and audit your mobile apps - plus drive browsers, run an 18-rule App Store compliance audit, and orchestrate multi-step batch flows.
11
11
 
12
12
  Distributed on the **public npm registry** - `npx @mmerterden/multi-agent-toolkit-mcp` resolves with no auth, no token, no `~/.npmrc` setup.
13
13
 
@@ -15,7 +15,7 @@ Distributed on the **public npm registry** - `npx @mmerterden/multi-agent-toolki
15
15
 
16
16
  That distinction is worth keeping straight. This line once called five hosts "the multi-agent-pipeline's full-orchestration targets", which stopped being true in pipeline v10.7.0 when the Cursor / Antigravity / Codex / Copilot Chat adapters were deleted. Codex CLI returned as a first-class target in pipeline v13.0.0 on capability - it now has skills, parallel sub-agents, hooks and MCP - and it is on the verified list only because a gate exercises it, not because it is supported elsewhere. Claiming hosts nobody exercises is not free either: 2.20.0 and 2.21.0 shipped an illegal `outputSchema` on the first tool, so Claude Code rejected the whole `tools/list` and served zero of the 78 - in the primary host, for two releases, with every gate in this repo green.
17
17
 
18
- **13 categories:**
18
+ **14 categories:**
19
19
  - **Device Control** (59 tools) - screenshot, tap, swipe, type, navigate, dark mode, locale, orientation, location, permissions, push notifications, real start/stop screen recording
20
20
  - **Memory** (2 tools) - `ios_leaks` runs /usr/bin/leaks against a simulator or host process, snapshot or diff against a saved memory graph; `android_meminfo` reads dumpsys meminfo, snapshot or diff. Both report `measurable:false` rather than a clean result when they could not look
21
21
  - **Crash Diagnostics** (2 tools) - `ios_list_crashes` reads the host's DiagnosticReports, `android_list_crashes` dumps the adb crash buffer, both tail-bounded
@@ -29,6 +29,7 @@ That distinction is worth keeping straight. This line once called five hosts "th
29
29
  - **Research** (2 tools) - provider-backed web search normalized to {title, url, snippet}, and a cited answer through Perplexity Sonar. Keys are read from the environment at call time and go straight into a header; a literal key is not an accepted argument, the same contract the Wallet signer uses
30
30
  - **Media** (1 tool) - `media_frames` pulls key frames out of a recording with ffmpeg, dropping near-duplicates, so the video `ios_record_video` just produced becomes something a model can actually look at
31
31
  - **Autonomous Agent DSL** (2 tools) - `agent_run_steps` executes a batch array of {tool, args, continue_on_error?, wait_ms?} steps in one MCP round trip. Ideal for scripted login flows, form fills, multi-step QA paths. `agent_query_output` searches the full output of an earlier call that was too large to return inline, so a follow-up question does not mean re-running an expensive tool.
32
+ - **Security** (2 tools) - offline, no egress: `security_cvss_score` turns a CVSS 3.1 base vector into a score and band by the FIRST arithmetic, so a finding's number cannot drift from its vector; `security_dep_inventory` normalizes a lockfile (npm, yarn, pnpm, CocoaPods, pip, Go, Cargo, RubyGems) into `{ecosystem, name, version}` for a separate CVE lookup to consume. Neither contacts a registry
32
33
 
33
34
  ## Quick Start
34
35
 
package/README.tr.md CHANGED
@@ -15,7 +15,7 @@ iOS Simulator, Android Emulator ve headless web kontrolü için **115 araç**. A
15
15
 
16
16
  Bu ayrımı net tutmakta fayda var. Bu satır bir zamanlar beş host'u "multi-agent-pipeline'ın tam-orkestrasyon hedefleri" olarak adlandırıyordu, bu da pipeline v10.7.0'da Cursor / Antigravity / Codex / Copilot Chat adaptörleri silindiğinde doğru olmaktan çıktı. Codex CLI, pipeline v13.0.0'da yetenek üzerinden birinci-sınıf bir hedef olarak geri döndü - artık skill'leri, paralel sub-agent'ları, hook'ları ve MCP'si var - ve doğrulanmış listede sadece bir kapı onu çalıştırdığı için yer alıyor, başka bir yerde desteklendiği için değil. Kimsenin çalıştırmadığı host'ları iddia etmek de bedavaya gelmiyor: 2.20.0 ve 2.21.0, ilk araçta yasa dışı bir `outputSchema` gönderdi, bu yüzden Claude Code tüm `tools/list`'i reddetti ve 78'in sıfırını sundu - birincil host'ta, iki release boyunca, bu repo'daki her kapı yeşilken.
17
17
 
18
- **13 kategori:**
18
+ **14 kategori:**
19
19
  - **Device Control** (59 araç) - screenshot, tap, swipe, type, navigate, dark mode, locale, orientation, location, permissions, push notifications, gerçek start/stop ekran kaydı
20
20
  - **Memory** (2 araç) - `ios_leaks`, bir simülatör ya da host süreci üzerinde /usr/bin/leaks çalıştırır; anlık görüntü alır ya da kayıtlı bir memory graph ile karşılaştırır. `android_meminfo`, dumpsys meminfo okur, aynı şekilde anlık görüntü ya da fark. İkisi de bakamadıklarında temiz bir sonuç değil `measurable:false` döner
21
21
  - **Crash Diagnostics** (2 araç) - `ios_list_crashes` host'un DiagnosticReports dizinini okur, `android_list_crashes` adb crash buffer'ını döker, ikisi de tail-sınırlı
@@ -29,6 +29,7 @@ Bu ayrımı net tutmakta fayda var. Bu satır bir zamanlar beş host'u "multi-ag
29
29
  - **Research** (2 araç) - sağlayıcı destekli web araması, {title, url, snippet} şemasına normalize edilmiş; ve Perplexity Sonar üzerinden atıflı cevap. Anahtarlar çağrı anında ortamdan okunup doğrudan bir başlığa gider; düz metin anahtar kabul edilen bir argüman değil - Wallet imzalayıcısının kullandığı sözleşmenin aynısı
30
30
  - **Media** (1 araç) - `media_frames`, bir kaydın anahtar karelerini ffmpeg ile çıkarır ve birbirine benzeyenleri eler; böylece `ios_record_video`'nun ürettiği video bir modelin gerçekten bakabileceği bir şeye dönüşür
31
31
  - **Autonomous Agent DSL** (2 araç) - `agent_run_steps`, tek bir MCP round trip'inde {tool, args, continue_on_error?, wait_ms?} adımlarından oluşan bir batch dizisini çalıştırır. Scriptlenmiş login akışları, form doldurma, çok-adımlı QA yolları için ideal. `agent_query_output`, satır içi dönemeyecek kadar büyük olan önceki bir çağrının tam çıktısını arar; böylece bir takip sorusu pahalı aracı yeniden koşturmak anlamına gelmez.
32
+ - **Security** (2 araç) - çevrimdışı, egress yok: `security_cvss_score` bir CVSS 3.1 temel vektörünü FIRST aritmetiğiyle skora ve banda çevirir, böylece bir bulgunun sayısı vektöründen sapamaz; `security_dep_inventory` bir lockfile'ı (npm, yarn, pnpm, CocoaPods, pip, Go, Cargo, RubyGems) `{ecosystem, name, version}` listesine normalize eder, ayrı bir CVE aramasının tüketmesi için. İkisi de bir registry'ye bağlanmaz
32
33
 
33
34
  ## Hızlı Başlangıç
34
35
 
package/index.js CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  shutdownAllLsp,
47
47
  } from "./tools/code-intel/index.js";
48
48
  import { PASS_TOOLS, handlePass, PASS_READ_ONLY, PASS_OUTPUT_SCHEMAS } from "./tools/pass-kit/index.js";
49
+ import { SECURITY_TOOLS, handleSecurity, SECURITY_OUTPUT_SCHEMAS } from "./tools/security/index.js";
49
50
  import { parseLaunchOutput } from "./tools/launch-time/index.js";
50
51
  import { parseLeaksOutput, parseMeminfoOutput, diffMeminfo } from "./tools/memory/index.js";
51
52
  import { auditIosTree, auditAndroidDump, parseAuditResults } from "./tools/a11y/index.js";
@@ -2499,10 +2500,10 @@ async function handleMedia(name, args) {
2499
2500
  //
2500
2501
  // Unset means everything, which is the behaviour every existing consumer
2501
2502
  // already has. The value is a comma-separated list of families: ios, android,
2502
- // web, design, code, pass, agent, context, research, media. An unknown name is
2503
+ // web, design, code, pass, agent, context, research, media, security. An unknown name is
2503
2504
  // reported on stderr rather than silently ignored, because a typo that quietly
2504
2505
  // disables a family is worse than a noisy one.
2505
- const ALL_CAPS = ["ios", "android", "web", "design", "code", "pass", "agent", "context", "research", "media"];
2506
+ const ALL_CAPS = ["ios", "android", "web", "design", "code", "pass", "agent", "context", "research", "media", "security"];
2506
2507
 
2507
2508
  function enabledCaps() {
2508
2509
  const raw = (process.env.MCP_TOOLKIT_CAPS || "").trim();
@@ -2543,6 +2544,7 @@ const ALL_TOOLS = [
2543
2544
  ...DESIGN_TOOLS,
2544
2545
  ...CODE_TOOLS,
2545
2546
  ...PASS_TOOLS,
2547
+ ...SECURITY_TOOLS,
2546
2548
  ];
2547
2549
 
2548
2550
  // Name -> inputSchema, so the CallTool boundary can enforce the declared shape.
@@ -2805,6 +2807,7 @@ const OUTPUT_SCHEMAS = {
2805
2807
  // made all 78 unavailable in Claude Code - see the note further down.
2806
2808
  ...CODE_OUTPUT_SCHEMAS,
2807
2809
  ...PASS_OUTPUT_SCHEMAS,
2810
+ ...SECURITY_OUTPUT_SCHEMAS,
2808
2811
  ios_accessibility_audit: ACCESSIBILITY_AUDIT_SCHEMA,
2809
2812
  android_accessibility_audit: ACCESSIBILITY_AUDIT_SCHEMA,
2810
2813
 
@@ -3019,6 +3022,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
3019
3022
  // aborted call cancels the in-flight LSP request instead of orphaning it.
3020
3023
  else if (name.startsWith("code_")) result = await handleCode(name, args || {}, ctx);
3021
3024
  else if (name.startsWith("pass_")) result = await handlePass(name, args || {});
3025
+ else if (name.startsWith("security_")) result = await handleSecurity(name, args || {});
3022
3026
  else return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
3023
3027
  // Same reason as dispatchStep: a handler returns null only from its
3024
3028
  // `default:` arm, so an unrecognised name that happens to carry a known
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-toolkit-mcp",
3
- "version": "3.13.1",
4
- "description": "MCP server for iOS Simulator, Android Emulator and headless web control. 115 tools: device automation (tap/swipe/type), accessibility audits, visual diff, crash logs, App Store / Play Store pre-submission compliance. Runs standalone over stdio with any MCP client.",
3
+ "version": "3.14.0",
4
+ "description": "MCP server for iOS Simulator, Android Emulator and headless web control. 117 tools: device automation (tap/swipe/type), accessibility audits, visual diff, crash logs, App Store / Play Store pre-submission compliance, offline CVSS scoring + dependency inventory. Runs standalone over stdio with any MCP client.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "start": "node index.js",
13
- "test": "node --test tools/design-check/__tests__/design-check.test.mjs tools/design-check/__tests__/plan-determinism.test.mjs tools/ios-app-store-audit/__tests__/app-store-audit.test.mjs tools/ios-testflight/__tests__/testflight.test.mjs tools/ui-inspect/__tests__/ui-inspect.test.mjs tools/code-intel/__tests__/code-intel.test.mjs tools/pass-kit/__tests__/pass-kit.test.mjs tools/crash-logs/__tests__/crash-logs.test.mjs tools/a11y/__tests__/a11y.test.mjs tools/launch-time/__tests__/launch-time.test.mjs tools/memory/__tests__/memory.test.mjs tools/offload/__tests__/offload.test.mjs tools/context/__tests__/context.test.mjs __tests__/server-tools.test.mjs __tests__/injection.test.mjs",
13
+ "test": "node --test tools/design-check/__tests__/design-check.test.mjs tools/design-check/__tests__/plan-determinism.test.mjs tools/ios-app-store-audit/__tests__/app-store-audit.test.mjs tools/ios-testflight/__tests__/testflight.test.mjs tools/ui-inspect/__tests__/ui-inspect.test.mjs tools/code-intel/__tests__/code-intel.test.mjs tools/pass-kit/__tests__/pass-kit.test.mjs tools/crash-logs/__tests__/crash-logs.test.mjs tools/a11y/__tests__/a11y.test.mjs tools/launch-time/__tests__/launch-time.test.mjs tools/memory/__tests__/memory.test.mjs tools/offload/__tests__/offload.test.mjs tools/context/__tests__/context.test.mjs tools/security/__tests__/security.test.mjs __tests__/server-tools.test.mjs __tests__/injection.test.mjs",
14
14
  "gates": "bash scripts/gates.sh"
15
15
  },
16
16
  "keywords": [
@@ -0,0 +1,108 @@
1
+ /**
2
+ * CVSS 3.1 base-score computation. Pure arithmetic from the specification
3
+ * (https://www.first.org/cvss/v3.1/specification-document), no dependency and
4
+ * no network. Given a base vector string it returns the base score and the
5
+ * qualitative band, which is what the pipeline maps onto its reviewer severity.
6
+ *
7
+ * @module tools/security/cvss
8
+ */
9
+
10
+ const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
11
+ const AC = { L: 0.77, H: 0.44 };
12
+ const UI = { N: 0.85, R: 0.62 };
13
+ // Privileges Required depends on Scope: the Changed column is the second entry.
14
+ const PR = { N: [0.85, 0.85], L: [0.62, 0.68], H: [0.27, 0.5] };
15
+ const CIA = { H: 0.56, L: 0.22, N: 0 };
16
+
17
+ const BASE_METRICS = ["AV", "AC", "PR", "UI", "S", "C", "I", "A"];
18
+ const ALLOWED = {
19
+ AV: ["N", "A", "L", "P"],
20
+ AC: ["L", "H"],
21
+ PR: ["N", "L", "H"],
22
+ UI: ["N", "R"],
23
+ S: ["U", "C"],
24
+ C: ["N", "L", "H"],
25
+ I: ["N", "L", "H"],
26
+ A: ["N", "L", "H"],
27
+ };
28
+
29
+ /**
30
+ * CVSS 3.1 Roundup: the smallest 1-decimal number >= input, computed in integer
31
+ * space so float representation cannot round 4.0 up to 4.1. This is the exact
32
+ * algorithm from the specification's appendix.
33
+ * @param {number} input
34
+ * @returns {number}
35
+ */
36
+ export function roundup(input) {
37
+ const intInput = Math.round(input * 100000);
38
+ if (intInput % 10000 === 0) return intInput / 100000;
39
+ return (Math.floor(intInput / 10000) + 1) / 10;
40
+ }
41
+
42
+ /**
43
+ * @param {number} score
44
+ * @returns {"none"|"low"|"medium"|"high"|"critical"}
45
+ */
46
+ export function band(score) {
47
+ if (score === 0) return "none";
48
+ if (score <= 3.9) return "low";
49
+ if (score <= 6.9) return "medium";
50
+ if (score <= 8.9) return "high";
51
+ return "critical";
52
+ }
53
+
54
+ /**
55
+ * Parse a CVSS 3.1 base vector into its metric map, validating the prefix, that
56
+ * every base metric is present exactly once, and that each value is legal.
57
+ * @param {string} vector
58
+ * @returns {{ metrics: Record<string,string> } | { error: string }}
59
+ */
60
+ export function parseVector(vector) {
61
+ if (typeof vector !== "string" || !vector.trim()) return { error: "vector is required" };
62
+ const parts = vector.trim().split("/");
63
+ if (parts[0] !== "CVSS:3.1") return { error: `vector must start with CVSS:3.1, got "${parts[0]}"` };
64
+ const metrics = {};
65
+ for (const part of parts.slice(1)) {
66
+ const [key, value] = part.split(":");
67
+ if (!key || value === undefined) return { error: `malformed metric segment "${part}"` };
68
+ if (metrics[key] !== undefined) return { error: `metric ${key} appears more than once` };
69
+ metrics[key] = value;
70
+ }
71
+ for (const m of BASE_METRICS) {
72
+ if (metrics[m] === undefined) return { error: `missing base metric ${m}` };
73
+ if (!ALLOWED[m].includes(metrics[m])) {
74
+ return { error: `illegal value ${m}:${metrics[m]} (allowed: ${ALLOWED[m].join("/")})` };
75
+ }
76
+ }
77
+ return { metrics };
78
+ }
79
+
80
+ /**
81
+ * Compute the CVSS 3.1 base score and band from a base vector.
82
+ * @param {string} vector
83
+ * @returns {{ vector: string, baseScore: number, band: string } | { error: string }}
84
+ */
85
+ export function scoreVector(vector) {
86
+ const parsed = parseVector(vector);
87
+ if ("error" in parsed) return parsed;
88
+ const m = parsed.metrics;
89
+ const scopeChanged = m.S === "C";
90
+
91
+ const iss = 1 - (1 - CIA[m.C]) * (1 - CIA[m.I]) * (1 - CIA[m.A]);
92
+ const impact = scopeChanged
93
+ ? 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15)
94
+ : 6.42 * iss;
95
+
96
+ const exploitability = 8.22 * AV[m.AV] * AC[m.AC] * PR[m.PR][scopeChanged ? 1 : 0] * UI[m.UI];
97
+
98
+ let baseScore;
99
+ if (impact <= 0) {
100
+ baseScore = 0;
101
+ } else if (scopeChanged) {
102
+ baseScore = roundup(Math.min(1.08 * (impact + exploitability), 10));
103
+ } else {
104
+ baseScore = roundup(Math.min(impact + exploitability, 10));
105
+ }
106
+
107
+ return { vector: vector.trim(), baseScore, band: band(baseScore) };
108
+ }
Binary file
@@ -0,0 +1,115 @@
1
+ /**
2
+ * security - local, offline helpers for a static security review.
3
+ *
4
+ * OFFLINE BY CONSTRUCTION. Neither tool contacts a registry, an advisory
5
+ * database or anything else: security_cvss_score is pure arithmetic, and
6
+ * security_dep_inventory reads one lockfile off disk and normalizes it. Matching
7
+ * an inventory against known CVEs is a separate, network-bearing step
8
+ * (ai-analyst-toolkit:evidence-registry) kept out of this family on purpose, so
9
+ * the toolkit's stdio-only egress allowlist does not change.
10
+ *
11
+ * GENERIC. No project, brand or repo appears here. Every input is a caller value.
12
+ *
13
+ * @module tools/security
14
+ */
15
+
16
+ import { scoreVector } from "./cvss.js";
17
+ import { inventory, SUPPORTED } from "./deps.js";
18
+
19
+ const ERROR_PREFIX = "ERROR: ";
20
+
21
+ export const SECURITY_TOOLS = [
22
+ {
23
+ name: "security_cvss_score",
24
+ description:
25
+ "Compute the CVSS 3.1 base score and qualitative band from a base vector string, so a finding's score cannot drift from its vector by hand. Pure arithmetic from the FIRST specification, offline. Returns { vector, baseScore, band } where band is none|low|medium|high|critical - the value the pipeline maps to its reviewer severity (critical/high -> blocking, medium -> important, low/none -> suggestion).",
26
+ inputSchema: {
27
+ type: "object",
28
+ additionalProperties: false,
29
+ required: ["vector"],
30
+ properties: {
31
+ vector: {
32
+ type: "string",
33
+ description:
34
+ "A full CVSS 3.1 base vector, e.g. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. All eight base metrics required; temporal and environmental metrics are ignored.",
35
+ },
36
+ },
37
+ },
38
+ },
39
+ {
40
+ name: "security_dep_inventory",
41
+ description:
42
+ "Parse a dependency lockfile off disk into a normalized inventory of { ecosystem, name, version }. Local parse only - contacts nothing. Supported lockfiles: " +
43
+ Object.keys(SUPPORTED).join(", ") +
44
+ ". Hand the inventory to a CVE lookup (ai-analyst-toolkit:evidence-registry) as a separate step; this tool never reaches the network.",
45
+ inputSchema: {
46
+ type: "object",
47
+ additionalProperties: false,
48
+ required: ["path"],
49
+ properties: {
50
+ path: {
51
+ type: "string",
52
+ description:
53
+ "Absolute path to a supported lockfile (its basename decides the parser, e.g. package-lock.json, Podfile.lock, requirements.txt, go.sum).",
54
+ },
55
+ },
56
+ },
57
+ },
58
+ ];
59
+
60
+ export const SECURITY_OUTPUT_SCHEMAS = {
61
+ security_cvss_score: {
62
+ type: "object",
63
+ required: ["vector", "baseScore", "band"],
64
+ properties: {
65
+ vector: { type: "string" },
66
+ baseScore: { type: "number" },
67
+ band: { type: "string", enum: ["none", "low", "medium", "high", "critical"] },
68
+ },
69
+ },
70
+ security_dep_inventory: {
71
+ type: "object",
72
+ required: ["ecosystem", "file", "count", "dependencies"],
73
+ properties: {
74
+ ecosystem: { type: "string" },
75
+ file: { type: "string" },
76
+ count: { type: "integer" },
77
+ dependencies: {
78
+ type: "array",
79
+ items: {
80
+ type: "object",
81
+ required: ["ecosystem", "name", "version"],
82
+ properties: {
83
+ ecosystem: { type: "string" },
84
+ name: { type: "string" },
85
+ version: { type: "string" },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ };
92
+
93
+ /**
94
+ * @param {string} name
95
+ * @param {Record<string, unknown>} args
96
+ * @returns {Promise<string|null>} JSON string result, or an ERROR: string, or null for an unknown name.
97
+ */
98
+ export async function handleSecurity(name, args = {}) {
99
+ try {
100
+ switch (name) {
101
+ case "security_cvss_score": {
102
+ const r = scoreVector(args.vector);
103
+ return "error" in r ? `${ERROR_PREFIX}${r.error}` : JSON.stringify(r);
104
+ }
105
+ case "security_dep_inventory": {
106
+ const r = inventory(args.path);
107
+ return "error" in r ? `${ERROR_PREFIX}${r.error}` : JSON.stringify(r);
108
+ }
109
+ default:
110
+ return null;
111
+ }
112
+ } catch (e) {
113
+ return `${ERROR_PREFIX}${name}: ${e?.message || String(e)}`;
114
+ }
115
+ }