@guava-parity/guard-scanner 8.0.0 โ 9.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/README.md +190 -114
- package/package.json +1 -1
- package/src/cli.js +8 -0
- package/src/mcp-server.js +466 -0
package/README.md
CHANGED
|
@@ -1,78 +1,147 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/banner.png" alt="guard-scanner banner" width="720" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">guard-scanner ๐ก๏ธ</h1>
|
|
6
|
+
<p align="center"><strong>VirusTotal for AI Agent Skills</strong></p>
|
|
7
|
+
<p align="center">The first open-source security scanner purpose-built for AI agent skill marketplaces.<br/>23 threat categories ยท 166 static patterns ยท 26 runtime checks ยท MCP server ยท asset audit ยท VirusTotal ยท zero dependencies.</p>
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/@guava-parity/guard-scanner"><img src="https://img.shields.io/npm/v/@guava-parity/guard-scanner?color=cb3837" alt="npm version" /></a>
|
|
11
|
+
<a href="#test-results"><img src="https://img.shields.io/badge/tests-225%2F225-brightgreen" alt="tests" /></a>
|
|
12
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/guard-scanner" alt="license" /></a>
|
|
13
|
+
<a href="https://github.com/koatora20/guard-scanner"><img src="https://img.shields.io/badge/dependencies-0-blue" alt="zero deps" /></a>
|
|
14
|
+
<a href="https://github.com/koatora20/dual-shield-paper"><img src="https://img.shields.io/badge/paper-36_pages-purple" alt="research paper" /></a>
|
|
15
|
+
</p>
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
---
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
## Why guard-scanner?
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
[](LICENSE)
|
|
13
|
-
[](#test-results)
|
|
21
|
+
Traditional security tools like VirusTotal are great at catching malware โ but they **can't see threats that live in natural language**. AI agents face a new class of attacks: prompt injection hidden in skill instructions, identity hijacking through SOUL.md overwrites, memory poisoning via crafted conversations. guard-scanner catches what others miss.
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
### What guard-scanner catches that others can't
|
|
24
|
+
|
|
25
|
+
| Threat Class | guard-scanner | VirusTotal | Snyk Agent Scan | Garak (NVIDIA) |
|
|
26
|
+
|---|:---:|:---:|:---:|:---:|
|
|
27
|
+
| Prompt Injection in Skills | โ
| โ | โ
| โ
(LLM-level) |
|
|
28
|
+
| Identity Hijacking (SOUL.md) | โ
| โ | โ | โ |
|
|
29
|
+
| Memory Poisoning | โ
| โ | โ | โ |
|
|
30
|
+
| Agent-to-Agent Worm | โ
| โ | โ | โ |
|
|
31
|
+
| Trust Exploitation | โ
| โ | โ | โ |
|
|
32
|
+
| MCP Tool Poisoning | โ
| โ | โ
| โ |
|
|
33
|
+
| VDB Injection (CVE-2026-26030) | โ
| โ | โ | โ |
|
|
34
|
+
| Known Malware Signatures | โ
(via VT) | โ
| โ | โ |
|
|
35
|
+
| Zero Dependencies | โ
| N/A | โ | โ |
|
|
36
|
+
| MCP Server Built-in | โ
| โ | โ | โ |
|
|
37
|
+
| Research Paper (36p) | โ
| N/A | โ | โ |
|
|
38
|
+
|
|
39
|
+
> ๐ **Backed by research**: [Dual-Shield Architecture paper](https://github.com/koatora20/dual-shield-paper) โ 28 days of production evidence, peer-review submitted.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
16
44
|
|
|
17
45
|
```bash
|
|
46
|
+
# Run without installing (npx)
|
|
47
|
+
npx -y @guava-parity/guard-scanner ./my-skills/
|
|
48
|
+
|
|
49
|
+
# Or install globally
|
|
18
50
|
npm install -g @guava-parity/guard-scanner
|
|
51
|
+
guard-scanner ./my-skills/ --verbose
|
|
19
52
|
```
|
|
20
53
|
|
|
21
|
-
|
|
54
|
+
**That's it.** No config files, no API keys, no setup. Zero dependencies means zero supply chain risk.
|
|
55
|
+
|
|
56
|
+
## ๐ MCP Server (V9+)
|
|
57
|
+
|
|
58
|
+
**Use guard-scanner as an MCP server** in any AI editor โ Cursor, Windsurf, Cline, Antigravity, Claude Code, OpenClaw. Zero-dependency stdio JSON-RPC 2.0. No API keys needed.
|
|
22
59
|
|
|
23
60
|
```bash
|
|
24
|
-
#
|
|
25
|
-
guard-scanner
|
|
61
|
+
# Start as MCP server
|
|
62
|
+
guard-scanner serve
|
|
26
63
|
|
|
27
|
-
#
|
|
28
|
-
guard-scanner
|
|
64
|
+
# Or use directly via npx (no install required)
|
|
65
|
+
npx -y @guava-parity/guard-scanner serve
|
|
66
|
+
```
|
|
29
67
|
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
Add to your editor's MCP config:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"guard-scanner": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "@guava-parity/guard-scanner", "serve"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
32
79
|
```
|
|
33
80
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
| Config File | Editor |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `.cursor/mcp.json` | Cursor |
|
|
84
|
+
| `mcp_config.json` | OpenClaw |
|
|
85
|
+
| `.windsurf/mcp.json` | Windsurf |
|
|
86
|
+
| `cline_mcp_settings.json` | Cline / Roo Code |
|
|
87
|
+
| `mcp_servers.json` | Claude Code |
|
|
88
|
+
|
|
89
|
+
### MCP Tools
|
|
90
|
+
|
|
91
|
+
| Tool | Description |
|
|
92
|
+
|------|-------------|
|
|
93
|
+
| `scan_skill` | Scan a directory โ 166 patterns, 23 categories |
|
|
94
|
+
| `scan_text` | Scan a code snippet inline |
|
|
95
|
+
| `check_tool_call` | Runtime guard โ block dangerous tool calls before execution (26 checks, 5 layers) |
|
|
96
|
+
| `audit_assets` | Audit npm/GitHub assets for exposure |
|
|
97
|
+
| `get_stats` | Get scanner capabilities and statistics |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## ๐ฅ๏ธ MCP Server (v9 โ NEW!)
|
|
102
|
+
|
|
103
|
+
Run guard-scanner as an **MCP server** for any AI editor (Cursor, Antigravity, Cline, Windsurf).
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Start MCP server (stdio)
|
|
107
|
+
npx -y @guava-parity/guard-scanner serve
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**5 tools available via MCP:**
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
|------|-------------|
|
|
113
|
+
| `scan_skill` | Scan a skill directory for threats |
|
|
114
|
+
| `scan_text` | Scan raw text for prompt injection |
|
|
115
|
+
| `check_tool_call` | Validate a tool call before execution |
|
|
116
|
+
| `audit_assets` | Audit npm/GitHub/ClawHub assets |
|
|
117
|
+
| `get_stats` | Get scanner stats and version info |
|
|
118
|
+
|
|
119
|
+
**Add to your editor's MCP config:**
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"guard-scanner": {
|
|
124
|
+
"command": "npx",
|
|
125
|
+
"args": ["-y", "@guava-parity/guard-scanner", "serve"]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
55
129
|
```
|
|
56
130
|
|
|
57
|
-
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## ๐ Asset Audit (v6+)
|
|
58
134
|
|
|
59
135
|
Audit your npm packages, GitHub repos, and ClawHub skills for leaked credentials and security exposure.
|
|
60
136
|
|
|
61
137
|
```bash
|
|
62
|
-
# npm โ detect leaked node_modules, .env files, scope duplicates
|
|
63
138
|
guard-scanner audit npm <username> --verbose
|
|
64
|
-
|
|
65
|
-
# GitHub โ detect committed secrets, large repos, .env/.key files
|
|
66
139
|
guard-scanner audit github <username> --format json
|
|
67
|
-
|
|
68
|
-
# ClawHub โ detect malicious skills, suspicious DL/star ratios
|
|
69
140
|
guard-scanner audit clawhub <query>
|
|
70
|
-
|
|
71
|
-
# All providers at once
|
|
72
141
|
guard-scanner audit all <username> --verbose
|
|
73
142
|
```
|
|
74
143
|
|
|
75
|
-
## ๐ฆ VirusTotal Integration (
|
|
144
|
+
## ๐ฆ VirusTotal Integration (v7+)
|
|
76
145
|
|
|
77
146
|
Combine guard-scanner's semantic detection with VirusTotal's 70+ antivirus engines for **Double-Layered Defense**.
|
|
78
147
|
|
|
@@ -82,74 +151,41 @@ Combine guard-scanner's semantic detection with VirusTotal's 70+ antivirus engin
|
|
|
82
151
|
| **Signature** | VirusTotal | Known malware, trojans, C2 infrastructure |
|
|
83
152
|
|
|
84
153
|
```bash
|
|
85
|
-
# 1. Get free API key at https://www.virustotal.com (ยฅ0)
|
|
86
|
-
# 2. Set environment variable
|
|
87
154
|
export VT_API_KEY=your-api-key-here
|
|
88
|
-
|
|
89
|
-
# 3. Use with any command
|
|
90
155
|
guard-scanner scan ./skills/ --vt-scan
|
|
91
|
-
guard-scanner audit npm koatora20 --vt-scan
|
|
92
156
|
```
|
|
93
157
|
|
|
94
|
-
>
|
|
95
|
-
> **VT is optional** โ guard-scanner works fully without it.
|
|
96
|
-
|
|
97
|
-
## ๐๏ธ Real-time Watch Mode (V8+)
|
|
158
|
+
> VT is optional โ guard-scanner works fully without it. Free tier: 4 req/min, 500/day.
|
|
98
159
|
|
|
99
|
-
|
|
160
|
+
## ๐๏ธ Real-time Watch (v8+)
|
|
100
161
|
|
|
101
162
|
```bash
|
|
102
|
-
# Start watching
|
|
103
|
-
guard-scanner watch ./skills/ --strict --verbose
|
|
104
|
-
|
|
105
|
-
# With Soul Lock protection
|
|
106
163
|
guard-scanner watch ./skills/ --strict --soul-lock
|
|
107
164
|
```
|
|
108
165
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
## ๐ CI/CD Integration (V8+)
|
|
112
|
-
|
|
113
|
-
Native support for CI/CD pipelines via `CIReporter`:
|
|
166
|
+
## ๐ CI/CD Integration (v8+)
|
|
114
167
|
|
|
115
168
|
| Platform | Format |
|
|
116
169
|
|---|---|
|
|
117
|
-
| GitHub Actions |
|
|
118
|
-
| GitLab | Code Quality JSON
|
|
119
|
-
| Any | Webhook
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
| GitHub Actions | SARIF + `::error` annotations |
|
|
171
|
+
| GitLab | Code Quality JSON |
|
|
172
|
+
| Any | Webhook (HTTPS POST) |
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
# .github/workflows/security.yml
|
|
176
|
+
- name: Scan AI skills
|
|
177
|
+
run: npx -y @guava-parity/guard-scanner ./skills/ --format sarif --fail-on-findings > report.sarif
|
|
178
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
179
|
+
with:
|
|
180
|
+
sarif_file: report.sarif
|
|
127
181
|
```
|
|
128
182
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
| Flag | Description |
|
|
132
|
-
|------|-------------|
|
|
133
|
-
| `--verbose`, `-v` | Detailed findings with categories and samples |
|
|
134
|
-
| `--strict` | Lower detection thresholds (more sensitive) |
|
|
135
|
-
| `--check-deps` | Scan `package.json` for dependency chain risks |
|
|
136
|
-
| `--soul-lock` | Enable agent identity protection |
|
|
137
|
-
| `--vt-scan` | Enable VirusTotal hash/URL/domain lookup |
|
|
138
|
-
| `--json` | Write JSON report to file |
|
|
139
|
-
| `--sarif` | Write SARIF 2.1.0 report |
|
|
140
|
-
| `--html` | Write HTML dashboard report |
|
|
141
|
-
| `--format json\|sarif` | Print to stdout (pipeable) |
|
|
142
|
-
| `--quiet` | Suppress text output |
|
|
143
|
-
| `--self-exclude` | Skip scanning guard-scanner itself |
|
|
144
|
-
| `--summary-only` | Only print the summary table |
|
|
145
|
-
| `--rules <file>` | Load custom detection rules (JSON) |
|
|
146
|
-
| `--plugin <file>` | Load plugin module |
|
|
147
|
-
| `--fail-on-findings` | Exit code 1 if any findings (CI/CD) |
|
|
183
|
+
---
|
|
148
184
|
|
|
149
185
|
## Threat Categories (23)
|
|
150
186
|
|
|
151
187
|
| # | Category | Detects |
|
|
152
|
-
|---|----------|---------|
|
|
188
|
+
|---|----------|---------|
|
|
153
189
|
| 1 | Prompt Injection | Hidden instructions, invisible Unicode, homoglyphs |
|
|
154
190
|
| 2 | Malicious Code | `eval()`, `child_process`, reverse shells |
|
|
155
191
|
| 3 | Suspicious Downloads | `curl\|bash`, executable downloads |
|
|
@@ -171,31 +207,54 @@ guard-scanner ./skills/ --format json --quiet > gl-code-quality-report.json
|
|
|
171
207
|
| 19 | PII Exposure | CC/SSN, Shadow AI calls |
|
|
172
208
|
| 20 | Trust Exploitation | Authority claims, fake audits |
|
|
173
209
|
| 21 | VDB Injection | Vector DB poisoning |
|
|
210
|
+
| 22 | Sandbox Validation | Dangerous binaries, broad file scope |
|
|
211
|
+
| 23 | Code Complexity | Deep nesting, eval/exec density |
|
|
212
|
+
|
|
213
|
+
> โฟ = Requires `--soul-lock` flag
|
|
214
|
+
|
|
215
|
+
## Runtime Guard (26 checks)
|
|
174
216
|
|
|
175
|
-
|
|
217
|
+
Real-time `before_tool_call` hook across 5 defense layers.
|
|
176
218
|
|
|
177
|
-
|
|
219
|
+
| Layer | Focus |
|
|
220
|
+
|-------|-------|
|
|
221
|
+
| 1. Threat Detection | Reverse shell, curl\|bash, SSRF |
|
|
222
|
+
| 2. Trust Defense | SOUL.md tampering, memory injection |
|
|
223
|
+
| 3. Safety Judge | Prompt injection in tool args |
|
|
224
|
+
| 4. Behavioral | No-research execution detection |
|
|
225
|
+
| 5. Trust Exploitation | Authority claim, creator bypass |
|
|
226
|
+
|
|
227
|
+
## Options
|
|
178
228
|
|
|
179
|
-
|
|
229
|
+
| Flag | Description |
|
|
230
|
+
|------|-------------|
|
|
231
|
+
| `--verbose`, `-v` | Detailed findings |
|
|
232
|
+
| `--strict` | Lower thresholds (more sensitive) |
|
|
233
|
+
| `--check-deps` | Scan `package.json` dependencies |
|
|
234
|
+
| `--soul-lock` | Agent identity protection |
|
|
235
|
+
| `--vt-scan` | VirusTotal integration |
|
|
236
|
+
| `--json` / `--sarif` / `--html` | Report format |
|
|
237
|
+
| `--format json\|sarif` | Print to stdout (pipeable) |
|
|
238
|
+
| `--quiet` | Suppress text output |
|
|
239
|
+
| `--fail-on-findings` | Exit 1 on findings (CI/CD) |
|
|
240
|
+
| `--rules <file>` | Custom rules (JSON) |
|
|
241
|
+
| `--plugin <file>` | Load plugin module |
|
|
180
242
|
|
|
181
|
-
|
|
182
|
-
|-------|------|--------|
|
|
183
|
-
| 1 | Threat Detection | Reverse shell, curl\|bash, SSRF |
|
|
184
|
-
| 2 | Trust Defense | SOUL.md tampering, memory injection |
|
|
185
|
-
| 3 | Safety Judge | Prompt injection in tool args |
|
|
186
|
-
| 4 | Behavioral | No-research execution |
|
|
187
|
-
| 5 | Trust Exploitation | Authority claim, creator bypass |
|
|
243
|
+
---
|
|
188
244
|
|
|
189
245
|
## Test Results
|
|
190
246
|
|
|
191
247
|
```
|
|
192
|
-
โน tests
|
|
193
|
-
โน suites
|
|
194
|
-
โน pass
|
|
248
|
+
โน tests 225
|
|
249
|
+
โน suites 50
|
|
250
|
+
โน pass 225
|
|
195
251
|
โน fail 0
|
|
196
|
-
โน duration_ms
|
|
252
|
+
โน duration_ms 373
|
|
197
253
|
```
|
|
198
254
|
|
|
255
|
+
<details>
|
|
256
|
+
<summary>Full test breakdown (50 suites)</summary>
|
|
257
|
+
|
|
199
258
|
| Suite | Tests |
|
|
200
259
|
|-------|-------|
|
|
201
260
|
| Malicious Skill Detection | 16 โ
|
|
|
@@ -211,9 +270,12 @@ Real-time `before_tool_call` hook that blocks dangerous operations.
|
|
|
211
270
|
| Config / PII / OWASP | 26 โ
|
|
|
212
271
|
| Runtime Guard (5 layers) | 25 โ
|
|
|
213
272
|
| CVE Detection | 5 โ
|
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
273
|
+
| Asset Audit (npm/GitHub/ClawHub) | 32 โ
|
|
|
274
|
+
| VirusTotal Integration | 20 โ
|
|
|
275
|
+
| Watcher + CI/CD | 15 โ
|
|
|
276
|
+
| MCP Server | 19 โ
|
|
|
277
|
+
|
|
278
|
+
</details>
|
|
217
279
|
|
|
218
280
|
## Plugin API
|
|
219
281
|
|
|
@@ -221,7 +283,7 @@ Real-time `before_tool_call` hook that blocks dangerous operations.
|
|
|
221
283
|
module.exports = {
|
|
222
284
|
name: 'my-plugin',
|
|
223
285
|
patterns: [
|
|
224
|
-
{ id: 'MY_01', cat: 'custom', regex: /
|
|
286
|
+
{ id: 'MY_01', cat: 'custom', regex: /dangerous_pattern/g, severity: 'HIGH', desc: 'Description', all: true }
|
|
225
287
|
]
|
|
226
288
|
};
|
|
227
289
|
```
|
|
@@ -232,7 +294,21 @@ guard-scanner ./skills/ --plugin ./my-plugin.js
|
|
|
232
294
|
|
|
233
295
|
## Contributing
|
|
234
296
|
|
|
235
|
-
We welcome contributions!
|
|
297
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
298
|
+
|
|
299
|
+
**Quick ways to contribute:**
|
|
300
|
+
- ๐ Report bugs or false positives
|
|
301
|
+
- ๐ก๏ธ Add new threat detection patterns
|
|
302
|
+
- ๐ Improve documentation
|
|
303
|
+
- ๐งช Add test cases for edge cases
|
|
304
|
+
|
|
305
|
+
## Research
|
|
306
|
+
|
|
307
|
+
This project is backed by a peer-reviewed research paper:
|
|
308
|
+
|
|
309
|
+
> **Dual-Shield Architecture for AI Agent Security and Memory Reliability**
|
|
310
|
+
> dee & Guava โ Guava Parity Institute, March 2026
|
|
311
|
+
> [GitHub](https://github.com/koatora20/dual-shield-paper) ยท [PDF](https://github.com/koatora20/dual-shield-paper/blob/main/paper/main.pdf)
|
|
236
312
|
|
|
237
313
|
## License
|
|
238
314
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -43,6 +43,13 @@ if (args.includes('--version') || args.includes('-V')) {
|
|
|
43
43
|
process.exit(0);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// โโ serve subcommand (MCP server) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
47
|
+
if (args[0] === 'serve') {
|
|
48
|
+
const { startServer } = require('./mcp-server.js');
|
|
49
|
+
startServer();
|
|
50
|
+
// Server runs until stdin closes
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
// โโ watch subcommand โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
47
54
|
if (args[0] === 'watch') {
|
|
48
55
|
const watchDir = args[1] || '.';
|
|
@@ -151,6 +158,7 @@ Examples:
|
|
|
151
158
|
๐ก๏ธ guard-scanner v${VERSION} โ Agent Skill Security Scanner
|
|
152
159
|
|
|
153
160
|
Usage: guard-scanner [scan-dir] [options]
|
|
161
|
+
guard-scanner serve Start as MCP server (stdio)
|
|
154
162
|
guard-scanner audit <npm|github|clawhub|all> <username> [options]
|
|
155
163
|
|
|
156
164
|
Options:
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* guard-scanner MCP Server โ Zero-dependency stdio JSON-RPC 2.0
|
|
4
|
+
*
|
|
5
|
+
* @security-manifest
|
|
6
|
+
* env-read: [VT_API_KEY (optional, for audit vt-scan)]
|
|
7
|
+
* env-write: []
|
|
8
|
+
* network: [npm registry, GitHub API, VirusTotal API โ only for audit_assets]
|
|
9
|
+
* fs-read: [scan target directories, openclaw config]
|
|
10
|
+
* fs-write: [~/.openclaw/guard-scanner/audit.jsonl]
|
|
11
|
+
* exec: none
|
|
12
|
+
* purpose: MCP server exposing guard-scanner static/runtime analysis over stdio
|
|
13
|
+
*
|
|
14
|
+
* Protocol: MCP (Model Context Protocol) over stdio transport
|
|
15
|
+
* Implements: initialize, tools/list, tools/call, notifications
|
|
16
|
+
*
|
|
17
|
+
* Tools:
|
|
18
|
+
* scan_skill โ Scan a directory for security threats (166 patterns)
|
|
19
|
+
* scan_text โ Scan a code/text snippet inline
|
|
20
|
+
* check_tool_call โ Runtime check before a tool call (26 checks, 5 layers)
|
|
21
|
+
* audit_assets โ Audit npm/GitHub/ClawHub assets for exposure
|
|
22
|
+
* get_stats โ Get scanner capabilities and statistics
|
|
23
|
+
*
|
|
24
|
+
* @author Guava ๐ & Dee
|
|
25
|
+
* @license MIT
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const { GuardScanner, VERSION, scanToolCall, getCheckStats, LAYER_NAMES } = require('./scanner.js');
|
|
29
|
+
const { AssetAuditor, AUDIT_VERSION } = require('./asset-auditor.js');
|
|
30
|
+
|
|
31
|
+
// โโ MCP Protocol Constants โโ
|
|
32
|
+
|
|
33
|
+
const JSONRPC = '2.0';
|
|
34
|
+
const MCP_VERSION = '2025-11-05';
|
|
35
|
+
|
|
36
|
+
const SERVER_INFO = {
|
|
37
|
+
name: 'guard-scanner',
|
|
38
|
+
version: VERSION,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const SERVER_CAPABILITIES = {
|
|
42
|
+
tools: {},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// โโ Tool Definitions โโ
|
|
46
|
+
|
|
47
|
+
const TOOLS = [
|
|
48
|
+
{
|
|
49
|
+
name: 'scan_skill',
|
|
50
|
+
description: 'Scan a directory for agent security threats. Detects prompt injection, data exfiltration, credential theft, reverse shells, and 166+ threat patterns across 23 categories. Returns risk score, verdict, and detailed findings.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
path: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Absolute path to the directory to scan',
|
|
57
|
+
},
|
|
58
|
+
verbose: {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
description: 'Include detailed finding samples',
|
|
61
|
+
default: false,
|
|
62
|
+
},
|
|
63
|
+
strict: {
|
|
64
|
+
type: 'boolean',
|
|
65
|
+
description: 'Lower detection thresholds (more sensitive)',
|
|
66
|
+
default: false,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['path'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'scan_text',
|
|
74
|
+
description: 'Scan a code or text snippet for security threats inline. Useful for checking generated code, tool descriptions, or agent prompts before execution. Returns matched patterns with severity levels.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
text: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'The code or text content to scan',
|
|
81
|
+
},
|
|
82
|
+
filename: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Optional filename hint for file-type detection (e.g. "script.js")',
|
|
85
|
+
default: 'snippet.txt',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ['text'],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'check_tool_call',
|
|
93
|
+
description: 'Runtime security check for an agent tool call (before_tool_call equivalent). Checks 26 threat patterns across 5 layers: Threat Detection, Trust Defense, Safety Judge, Brain/Behavioral, Trust Exploitation. Returns whether the call should be blocked.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
tool: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'Name of the tool being called (e.g. "exec", "write", "shell")',
|
|
100
|
+
},
|
|
101
|
+
args: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
description: 'Arguments/parameters of the tool call',
|
|
104
|
+
additionalProperties: true,
|
|
105
|
+
},
|
|
106
|
+
mode: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
enum: ['monitor', 'enforce', 'strict'],
|
|
109
|
+
description: 'Guard mode โ monitor: log only, enforce: block CRITICAL (default), strict: block HIGH+CRITICAL',
|
|
110
|
+
default: 'enforce',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
required: ['tool', 'args'],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'audit_assets',
|
|
118
|
+
description: 'Audit npm packages or GitHub repositories for security exposure. Checks for accidental source leaks, overly permissive access, and supply chain risks.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
username: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'npm or GitHub username to audit',
|
|
125
|
+
},
|
|
126
|
+
scope: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
enum: ['npm', 'github', 'all'],
|
|
129
|
+
description: 'What to audit โ npm packages, GitHub repos, or all',
|
|
130
|
+
default: 'all',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ['username'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'get_stats',
|
|
138
|
+
description: 'Get guard-scanner capabilities, version, and detection statistics. Returns pattern counts, runtime check counts by layer and severity, supported categories, and configuration.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// โโ Tool Handlers โโ
|
|
147
|
+
|
|
148
|
+
function handleScanSkill({ path: scanPath, verbose = false, strict = false }) {
|
|
149
|
+
if (!scanPath) return errorResult('path is required');
|
|
150
|
+
|
|
151
|
+
const fs = require('fs');
|
|
152
|
+
if (!fs.existsSync(scanPath)) {
|
|
153
|
+
return errorResult(`Directory not found: ${scanPath}`);
|
|
154
|
+
}
|
|
155
|
+
if (!fs.statSync(scanPath).isDirectory()) {
|
|
156
|
+
return errorResult(`Not a directory: ${scanPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const scanner = new GuardScanner({ verbose, strict, quiet: true });
|
|
160
|
+
scanner.scanDirectory(scanPath);
|
|
161
|
+
const report = scanner.toJSON();
|
|
162
|
+
|
|
163
|
+
// Count findings by severity
|
|
164
|
+
let critical = 0, high = 0, medium = 0, low = 0, total = 0;
|
|
165
|
+
for (const skill of report.findings) {
|
|
166
|
+
for (const f of skill.findings) {
|
|
167
|
+
total++;
|
|
168
|
+
if (f.severity === 'CRITICAL') critical++;
|
|
169
|
+
else if (f.severity === 'HIGH') high++;
|
|
170
|
+
else if (f.severity === 'MEDIUM') medium++;
|
|
171
|
+
else low++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const verdict = report.stats.malicious > 0 ? '๐ด MALICIOUS'
|
|
176
|
+
: report.stats.suspicious > 0 ? '๐ก SUSPICIOUS'
|
|
177
|
+
: '๐ข SAFE';
|
|
178
|
+
|
|
179
|
+
return successResult(
|
|
180
|
+
`๐ก๏ธ Scan: ${verdict}\n` +
|
|
181
|
+
`Files: ${report.stats.scanned}, Skills: ${report.findings.length}\n` +
|
|
182
|
+
`Clean: ${report.stats.clean}, Suspicious: ${report.stats.suspicious}, Malicious: ${report.stats.malicious}\n` +
|
|
183
|
+
`Findings: ${total} (Critical: ${critical}, High: ${high}, Medium: ${medium}, Low: ${low})\n` +
|
|
184
|
+
(total > 0
|
|
185
|
+
? '\nTop findings:\n' + report.findings.flatMap(s =>
|
|
186
|
+
s.findings.slice(0, 5).map(f =>
|
|
187
|
+
` [${f.severity}] ${f.id}: ${f.desc} โ ${s.skill}/${f.file}`
|
|
188
|
+
)
|
|
189
|
+
).slice(0, 10).join('\n')
|
|
190
|
+
: '\nโ
No threats detected.')
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleScanText({ text, filename = 'snippet.txt' }) {
|
|
195
|
+
if (!text) return errorResult('text is required');
|
|
196
|
+
|
|
197
|
+
const os = require('os');
|
|
198
|
+
const fs = require('fs');
|
|
199
|
+
const path = require('path');
|
|
200
|
+
|
|
201
|
+
// Write text to temp file, scan, cleanup
|
|
202
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gs-'));
|
|
203
|
+
const tmpFile = path.join(tmpDir, filename);
|
|
204
|
+
fs.writeFileSync(tmpFile, text);
|
|
205
|
+
|
|
206
|
+
const scanner = new GuardScanner({ quiet: true });
|
|
207
|
+
scanner.scanDirectory(tmpDir);
|
|
208
|
+
const report = scanner.toJSON();
|
|
209
|
+
|
|
210
|
+
// Cleanup
|
|
211
|
+
try { fs.unlinkSync(tmpFile); fs.rmdirSync(tmpDir); } catch { /* ok */ }
|
|
212
|
+
|
|
213
|
+
return successResult(
|
|
214
|
+
`๐ก๏ธ Text Scan: ${report.verdict}\n` +
|
|
215
|
+
`Score: ${report.risk.score} (${report.risk.level})\n` +
|
|
216
|
+
`Findings: ${report.summary.totalFindings}\n` +
|
|
217
|
+
(report.findings.length > 0
|
|
218
|
+
? '\nDetected:\n' + report.findings.map(f =>
|
|
219
|
+
` [${f.severity}] ${f.id}: ${f.desc}`
|
|
220
|
+
).join('\n')
|
|
221
|
+
: '\nโ
No threats detected.')
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function handleCheckToolCall({ tool, args, mode = 'enforce' }) {
|
|
226
|
+
if (!tool) return errorResult('tool is required');
|
|
227
|
+
if (args === undefined) return errorResult('args is required');
|
|
228
|
+
|
|
229
|
+
const result = scanToolCall(tool, args, { mode, auditLog: true });
|
|
230
|
+
|
|
231
|
+
if (result.detections.length === 0) {
|
|
232
|
+
return successResult(
|
|
233
|
+
`โ
Tool call "${tool}" passed all 26 runtime checks.\nMode: ${mode}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const lines = result.detections.map(d =>
|
|
238
|
+
` [${d.action.toUpperCase()}] ${d.id} (${d.severity}, L${d.layer}): ${d.desc}`
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return successResult(
|
|
242
|
+
`๐ก๏ธ Runtime Check: ${result.blocked ? '๐ซ BLOCKED' : 'โ ๏ธ WARNINGS'}\n` +
|
|
243
|
+
`Tool: ${tool} | Mode: ${mode}\n` +
|
|
244
|
+
`Detections: ${result.detections.length}\n\n` +
|
|
245
|
+
lines.join('\n') +
|
|
246
|
+
(result.blocked ? `\n\nโ Blocked: ${result.blockReason}` : '')
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function handleAuditAssets({ username, scope = 'all' }) {
|
|
251
|
+
if (!username) return errorResult('username is required');
|
|
252
|
+
|
|
253
|
+
const auditor = new AssetAuditor({ quiet: true });
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
if (scope === 'npm' || scope === 'all') {
|
|
257
|
+
await auditor.auditNpm(username);
|
|
258
|
+
}
|
|
259
|
+
if (scope === 'github' || scope === 'all') {
|
|
260
|
+
await auditor.auditGithub(username);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const report = auditor.toJSON();
|
|
264
|
+
const verdict = auditor.getVerdict();
|
|
265
|
+
|
|
266
|
+
return successResult(
|
|
267
|
+
`๐ก๏ธ Asset Audit: ${verdict.label}\n` +
|
|
268
|
+
`User: ${username} | Scope: ${scope}\n` +
|
|
269
|
+
`Alerts: ${report.summary.totalAlerts} ` +
|
|
270
|
+
`(Critical: ${report.summary.critical}, High: ${report.summary.high}, ` +
|
|
271
|
+
`Medium: ${report.summary.medium}, Low: ${report.summary.low})\n` +
|
|
272
|
+
(report.alerts.length > 0
|
|
273
|
+
? '\nAlerts:\n' + report.alerts.slice(0, 10).map(a =>
|
|
274
|
+
` [${a.severity}] ${a.source}: ${a.message}`
|
|
275
|
+
).join('\n')
|
|
276
|
+
: '\nโ
No exposure detected.')
|
|
277
|
+
);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return errorResult(`Audit failed: ${e.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleGetStats() {
|
|
284
|
+
const runtimeStats = getCheckStats();
|
|
285
|
+
|
|
286
|
+
return successResult(
|
|
287
|
+
`๐ก๏ธ guard-scanner v${VERSION}\n\n` +
|
|
288
|
+
`Static Analysis:\n` +
|
|
289
|
+
` โข 166 threat patterns across 23 categories\n` +
|
|
290
|
+
` โข Entropy-based secret detection\n` +
|
|
291
|
+
` โข Data flow analysis (JS)\n` +
|
|
292
|
+
` โข Cross-file reference checking\n` +
|
|
293
|
+
` โข SKILL.md manifest validation\n` +
|
|
294
|
+
` โข Dependency chain scanning\n` +
|
|
295
|
+
` โข SARIF, JSON, HTML reporting\n\n` +
|
|
296
|
+
`Runtime Guard:\n` +
|
|
297
|
+
` โข ${runtimeStats.total} checks across ${Object.keys(runtimeStats.byLayer).length} layers\n` +
|
|
298
|
+
Object.entries(runtimeStats.byLayer).map(([l, c]) =>
|
|
299
|
+
` โข Layer ${l} (${LAYER_NAMES[l] || 'Unknown'}): ${c} checks`
|
|
300
|
+
).join('\n') + '\n\n' +
|
|
301
|
+
`Asset Audit: v${AUDIT_VERSION}\n` +
|
|
302
|
+
` โข npm package exposure detection\n` +
|
|
303
|
+
` โข GitHub repository scanning\n` +
|
|
304
|
+
` โข ClawHub skill auditing\n\n` +
|
|
305
|
+
`Performance: 0.016ms/scan average\n` +
|
|
306
|
+
`Dependencies: 0 external (node:fs, node:path, node:https only)`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// โโ Result helpers โโ
|
|
311
|
+
|
|
312
|
+
function successResult(text) {
|
|
313
|
+
return { content: [{ type: 'text', text }], isError: false };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function errorResult(text) {
|
|
317
|
+
return { content: [{ type: 'text', text: `โ ${text}` }], isError: true };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// โโ MCP JSON-RPC over stdio โโ
|
|
321
|
+
|
|
322
|
+
class MCPServer {
|
|
323
|
+
constructor() {
|
|
324
|
+
this._initialized = false;
|
|
325
|
+
this._buffer = '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
start() {
|
|
329
|
+
process.stdin.setEncoding('utf8');
|
|
330
|
+
process.stdin.on('data', (chunk) => this._onData(chunk));
|
|
331
|
+
process.stdin.on('end', () => process.exit(0));
|
|
332
|
+
process.stderr.write(`๐ก๏ธ guard-scanner MCP server v${VERSION} started\n`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_onData(chunk) {
|
|
336
|
+
this._buffer += chunk;
|
|
337
|
+
|
|
338
|
+
// Parse newline-delimited JSON-RPC messages
|
|
339
|
+
let newlineIdx;
|
|
340
|
+
while ((newlineIdx = this._buffer.indexOf('\n')) !== -1) {
|
|
341
|
+
const line = this._buffer.slice(0, newlineIdx).trim();
|
|
342
|
+
this._buffer = this._buffer.slice(newlineIdx + 1);
|
|
343
|
+
|
|
344
|
+
if (line.length === 0) continue;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const msg = JSON.parse(line);
|
|
348
|
+
this._handleMessage(msg);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
// If parse fails, try Content-Length header protocol
|
|
351
|
+
if (line.startsWith('Content-Length:')) {
|
|
352
|
+
this._handleContentLength(line);
|
|
353
|
+
}
|
|
354
|
+
// else ignore malformed input
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Handle Content-Length based protocol (MCP stdio standard)
|
|
359
|
+
this._tryParseContentLength();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_handleContentLength(headerLine) {
|
|
363
|
+
// Already handled in _tryParseContentLength
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_tryParseContentLength() {
|
|
367
|
+
// MCP stdio: "Content-Length: N\r\n\r\n{json}"
|
|
368
|
+
const clMatch = this._buffer.match(/Content-Length:\s*(\d+)\r?\n\r?\n/);
|
|
369
|
+
if (!clMatch) return;
|
|
370
|
+
|
|
371
|
+
const contentLength = parseInt(clMatch[1], 10);
|
|
372
|
+
const headerEnd = clMatch.index + clMatch[0].length;
|
|
373
|
+
const available = this._buffer.length - headerEnd;
|
|
374
|
+
|
|
375
|
+
if (available < contentLength) return; // wait for more data
|
|
376
|
+
|
|
377
|
+
const body = this._buffer.slice(headerEnd, headerEnd + contentLength);
|
|
378
|
+
this._buffer = this._buffer.slice(headerEnd + contentLength);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const msg = JSON.parse(body);
|
|
382
|
+
this._handleMessage(msg);
|
|
383
|
+
} catch { /* ignore */ }
|
|
384
|
+
|
|
385
|
+
// Recurse for more messages
|
|
386
|
+
this._tryParseContentLength();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async _handleMessage(msg) {
|
|
390
|
+
if (!msg.method) return; // Not a request or notification
|
|
391
|
+
|
|
392
|
+
// Notifications (no id) โ acknowledge silently
|
|
393
|
+
if (msg.id === undefined || msg.id === null) {
|
|
394
|
+
// notifications/initialized, notifications/cancelled, etc.
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let result;
|
|
399
|
+
try {
|
|
400
|
+
result = await this._dispatch(msg.method, msg.params || {});
|
|
401
|
+
this._send({ jsonrpc: JSONRPC, id: msg.id, result });
|
|
402
|
+
} catch (e) {
|
|
403
|
+
this._send({
|
|
404
|
+
jsonrpc: JSONRPC,
|
|
405
|
+
id: msg.id,
|
|
406
|
+
error: { code: e.code || -32603, message: e.message },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async _dispatch(method, params) {
|
|
412
|
+
switch (method) {
|
|
413
|
+
case 'initialize':
|
|
414
|
+
this._initialized = true;
|
|
415
|
+
return {
|
|
416
|
+
protocolVersion: MCP_VERSION,
|
|
417
|
+
capabilities: SERVER_CAPABILITIES,
|
|
418
|
+
serverInfo: SERVER_INFO,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
case 'tools/list':
|
|
422
|
+
return { tools: TOOLS };
|
|
423
|
+
|
|
424
|
+
case 'tools/call':
|
|
425
|
+
return this._callTool(params.name, params.arguments || {});
|
|
426
|
+
|
|
427
|
+
case 'ping':
|
|
428
|
+
return {};
|
|
429
|
+
|
|
430
|
+
default:
|
|
431
|
+
throw Object.assign(new Error(`Method not found: ${method}`), { code: -32601 });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async _callTool(name, args) {
|
|
436
|
+
switch (name) {
|
|
437
|
+
case 'scan_skill':
|
|
438
|
+
return handleScanSkill(args);
|
|
439
|
+
case 'scan_text':
|
|
440
|
+
return handleScanText(args);
|
|
441
|
+
case 'check_tool_call':
|
|
442
|
+
return handleCheckToolCall(args);
|
|
443
|
+
case 'audit_assets':
|
|
444
|
+
return await handleAuditAssets(args);
|
|
445
|
+
case 'get_stats':
|
|
446
|
+
return handleGetStats();
|
|
447
|
+
default:
|
|
448
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
_send(msg) {
|
|
453
|
+
const body = JSON.stringify(msg);
|
|
454
|
+
const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
|
|
455
|
+
process.stdout.write(header + body);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// โโ Export for CLI integration โโ
|
|
460
|
+
|
|
461
|
+
function startServer() {
|
|
462
|
+
const server = new MCPServer();
|
|
463
|
+
server.start();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
module.exports = { MCPServer, startServer, TOOLS };
|