@skvil/mcp-server 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 +248 -0
- package/dist/api.js +123 -0
- package/dist/config.js +102 -0
- package/dist/index.js +29 -0
- package/dist/tools.js +351 -0
- package/dist/types.js +2 -0
- package/dist/version.js +2 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Skvil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# skvil-mcp
|
|
4
|
+
|
|
5
|
+
**MCP server for the Skvil security scanner**
|
|
6
|
+
|
|
7
|
+
Verify, scan, and check on-chain certifications for AI agent skills — directly from your AI assistant.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@skvil/mcp-server)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://modelcontextprotocol.io)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why skvil-mcp?
|
|
19
|
+
|
|
20
|
+
AI agents install skills from the internet — but how do you know a skill is safe?
|
|
21
|
+
|
|
22
|
+
Skvil is a community-powered security scanner that analyzes AI agent skills for malicious patterns, builds reputation scores through crowdsourced scans, and issues **on-chain certifications** that are tamper-proof and publicly verifiable.
|
|
23
|
+
|
|
24
|
+
This MCP server gives your AI agent native tools to interact with the Skvil network. No HTTP knowledge required — just ask your agent to verify a skill.
|
|
25
|
+
|
|
26
|
+
### On-chain certification
|
|
27
|
+
|
|
28
|
+
Skvil's certification pipeline is what sets it apart — the entire process is **fully automated with zero human intervention**:
|
|
29
|
+
|
|
30
|
+
1. **Community scanning** — multiple independent agents scan the same skill
|
|
31
|
+
2. **Reputation building** — scores aggregate via exponential moving average (EMA)
|
|
32
|
+
3. **Crucible analysis** — automated static analysis scans 32+ pattern categories, then an AI triage phase (embeddings + LLM) validates findings and filters false positives
|
|
33
|
+
4. **On-chain registration** — skills scoring ≥ 80 are automatically anchored on Solana via SPL Memo transactions, creating a tamper-proof trust anchor that no single party can forge or revoke silently
|
|
34
|
+
|
|
35
|
+
Certification is algorithmic: score ≥ 50 passes, score < 50 fails and revokes any existing certificate. A periodic re-certification scheduler re-analyzes certified skills and revokes those that no longer pass.
|
|
36
|
+
|
|
37
|
+
When you run `skvil_verify`, you're not just checking a database — you're verifying against an immutable on-chain record.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
### Claude Desktop
|
|
44
|
+
|
|
45
|
+
Add to your `claude_desktop_config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"skvil": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "@skvil/mcp-server"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Claude Code
|
|
59
|
+
|
|
60
|
+
Add to your project's `.mcp.json`:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"skvil": {
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["-y", "@skvil/mcp-server"]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### VS Code / Cursor
|
|
74
|
+
|
|
75
|
+
Add to your settings (JSON):
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcp.servers": {
|
|
80
|
+
"skvil": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": ["-y", "@skvil/mcp-server"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That's it. The server auto-registers a free API key on first use. Zero config.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Tools
|
|
93
|
+
|
|
94
|
+
| Tool | Auth | Description |
|
|
95
|
+
|------|------|-------------|
|
|
96
|
+
| `skvil_verify` | No | Check if a skill is safe by its SHA-256 hash. Returns reputation score, risk level, on-chain certification status, and Crucible behavioral analysis. |
|
|
97
|
+
| `skvil_stats` | No | Community statistics: total skills scanned, trusted, critical, and on-chain certified counts. |
|
|
98
|
+
| `skvil_certified` | No | List skills with active on-chain certifications (V1/V2/V3/Gold). Up to 10 most recent. |
|
|
99
|
+
| `skvil_register` | No | Get a free API key (500 scans/day). Auto-cached locally for future use. |
|
|
100
|
+
| `skvil_scan` | Key | Submit security scan results to the community reputation network. |
|
|
101
|
+
| `skvil_report` | Key | Report a suspicious skill. Confirmed reports trigger automatic on-chain revocation. |
|
|
102
|
+
|
|
103
|
+
### Certification levels
|
|
104
|
+
|
|
105
|
+
| Level | Meaning |
|
|
106
|
+
|-------|---------|
|
|
107
|
+
| **V1** | Basic verification — scanned by community, passed automated static analysis (32+ pattern categories + AI triage) |
|
|
108
|
+
| **V2** | Enhanced verification — V1 + passed Crucible behavioral analysis in sandboxed environment |
|
|
109
|
+
| **V3** | Full verification — V2 + passed periodic re-certification cycles |
|
|
110
|
+
| **Gold** | Highest trust — V3 + continuous monitoring, reserved for critical infrastructure skills |
|
|
111
|
+
|
|
112
|
+
All levels are registered on-chain. The entire certification process is automated — no human review is involved at any level. Higher levels require progressively more rigorous automated verification.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
### API key
|
|
119
|
+
|
|
120
|
+
The server automatically registers a free API key on first use and caches it in `~/.skvil/mcp-config.json`.
|
|
121
|
+
|
|
122
|
+
To use an existing key:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"skvil": {
|
|
128
|
+
"command": "npx",
|
|
129
|
+
"args": ["-y", "@skvil/mcp-server"],
|
|
130
|
+
"env": {
|
|
131
|
+
"SKVIL_API_KEY": "sk_your_key_here"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Custom API URL
|
|
139
|
+
|
|
140
|
+
For self-hosted or development instances:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"env": {
|
|
145
|
+
"SKVIL_API_URL": "http://localhost:8000"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Key resolution order
|
|
151
|
+
|
|
152
|
+
1. `SKVIL_API_KEY` environment variable
|
|
153
|
+
2. Cached key in `~/.skvil/mcp-config.json`
|
|
154
|
+
3. Legacy Python client key in `~/.skvil/config`
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Examples
|
|
159
|
+
|
|
160
|
+
### "Is this skill safe to install?"
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
User: Check if sha256:4a2f...c81e is safe
|
|
164
|
+
|
|
165
|
+
Agent calls skvil_verify → gets reputation, certification, findings
|
|
166
|
+
|
|
167
|
+
Agent: This skill has a reputation score of 91.2 (safe) and is
|
|
168
|
+
on-chain certified at V2 level. 15 community scans with
|
|
169
|
+
no critical findings. Safe to install.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### "What skills are certified?"
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
User: Show me certified skills
|
|
176
|
+
|
|
177
|
+
Agent calls skvil_certified → gets list of on-chain certified skills
|
|
178
|
+
|
|
179
|
+
Agent: There are 7 on-chain certified skills:
|
|
180
|
+
1. web-scraper — V2, score 91.2, certified Feb 18
|
|
181
|
+
2. data-pipeline — V1, score 85.0, certified Feb 15
|
|
182
|
+
...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### "Report this suspicious skill"
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
User: Report sha256:dead...beef — it's sending data to an unknown server
|
|
189
|
+
|
|
190
|
+
Agent calls skvil_report → submits report
|
|
191
|
+
|
|
192
|
+
Agent: Report #42 submitted. The skill will be re-analyzed
|
|
193
|
+
automatically. If the report is confirmed, the skill
|
|
194
|
+
will be flagged and any on-chain certification will
|
|
195
|
+
be revoked.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## How it works
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
┌─────────────┐ stdio ┌────────────┐ HTTPS ┌──────────────────┐
|
|
204
|
+
│ AI Agent │ ◄────────────► │ skvil-mcp │ ────────────► │ api.skvil.com │
|
|
205
|
+
│ (Claude, │ MCP tools │ (local) │ REST API │ (reputation DB │
|
|
206
|
+
│ GPT, etc) │ │ │ │ + on-chain) │
|
|
207
|
+
└─────────────┘ └────────────┘ └──────────────────┘
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The MCP server runs locally as a subprocess of your AI client. It translates MCP tool calls into HTTPS requests to the Skvil API. No data is stored remotely except scan results and reports — and certifications are anchored on-chain for public verification.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
git clone https://github.com/Skvil-IA/skvil-mcp.git
|
|
218
|
+
cd skvil-mcp
|
|
219
|
+
npm install
|
|
220
|
+
npm run build
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Run locally
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Point to local API for development
|
|
227
|
+
SKVIL_API_URL=http://localhost:8000 node dist/index.js
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Test with MCP Inspector
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Lint & format
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npm run lint
|
|
240
|
+
npm run format
|
|
241
|
+
npm run typecheck
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
[MIT](LICENSE) — Skvil 2026
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getApiKey, getBaseUrl, saveApiKey } from './config.js';
|
|
2
|
+
import { VERSION } from './version.js';
|
|
3
|
+
const USER_AGENT = `skvil-mcp/${VERSION}`;
|
|
4
|
+
const TIMEOUT_MS = 15_000;
|
|
5
|
+
const MAX_RESPONSE_BYTES = 1_048_576; // 1 MB
|
|
6
|
+
const RETRIABLE_STATUSES = new Set([502, 503, 504]);
|
|
7
|
+
const MAX_RETRIES = 2;
|
|
8
|
+
const BASE_DELAY_MS = 1_000;
|
|
9
|
+
export class SkvilApiError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
detail;
|
|
12
|
+
constructor(status, detail) {
|
|
13
|
+
super(`HTTP ${status}: ${detail}`);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.detail = detail;
|
|
16
|
+
this.name = 'SkvilApiError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function request(method, path, options = {}) {
|
|
20
|
+
const url = `${getBaseUrl()}${path}`;
|
|
21
|
+
const headers = {
|
|
22
|
+
'User-Agent': USER_AGENT,
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
};
|
|
25
|
+
if (options.body !== undefined) {
|
|
26
|
+
headers['Content-Type'] = 'application/json';
|
|
27
|
+
}
|
|
28
|
+
if (options.auth) {
|
|
29
|
+
const key = getApiKey();
|
|
30
|
+
if (!key) {
|
|
31
|
+
throw new SkvilApiError(401, 'No API key configured. Use skvil_register to get one, or set SKVIL_API_KEY env var.');
|
|
32
|
+
}
|
|
33
|
+
headers['X-API-Key'] = key;
|
|
34
|
+
}
|
|
35
|
+
let lastError;
|
|
36
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
37
|
+
if (attempt > 0) {
|
|
38
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
40
|
+
}
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
method,
|
|
46
|
+
headers,
|
|
47
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
48
|
+
signal: controller.signal,
|
|
49
|
+
});
|
|
50
|
+
// Guard against excessively large responses
|
|
51
|
+
const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
|
|
52
|
+
if (contentLength > MAX_RESPONSE_BYTES) {
|
|
53
|
+
throw new SkvilApiError(502, 'Response too large');
|
|
54
|
+
}
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
// Retry on transient server errors (not on 4xx client errors)
|
|
58
|
+
if (RETRIABLE_STATUSES.has(response.status) && attempt < MAX_RETRIES) {
|
|
59
|
+
lastError = new SkvilApiError(response.status, text.slice(0, 200));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let detail = text.slice(0, 500);
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(text);
|
|
65
|
+
detail = parsed.detail || detail;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Use raw text
|
|
69
|
+
}
|
|
70
|
+
throw new SkvilApiError(response.status, detail);
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(text);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// Retry on network errors (AbortError = timeout, TypeError = DNS/connection)
|
|
76
|
+
if (error instanceof Error &&
|
|
77
|
+
(error.name === 'AbortError' || error.name === 'TypeError') &&
|
|
78
|
+
attempt < MAX_RETRIES) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// All retries exhausted
|
|
89
|
+
throw lastError;
|
|
90
|
+
}
|
|
91
|
+
/** Check if a skill is safe by its composite hash. */
|
|
92
|
+
export async function verify(hash) {
|
|
93
|
+
return request('GET', `/verify/${encodeURIComponent(hash)}`);
|
|
94
|
+
}
|
|
95
|
+
/** Get community statistics. */
|
|
96
|
+
export async function stats() {
|
|
97
|
+
return request('GET', '/stats');
|
|
98
|
+
}
|
|
99
|
+
/** List actively certified skills. */
|
|
100
|
+
export async function certified() {
|
|
101
|
+
return request('GET', '/certified');
|
|
102
|
+
}
|
|
103
|
+
/** List all certified skills with full catalog metadata. */
|
|
104
|
+
export async function catalog() {
|
|
105
|
+
return request('GET', '/catalog');
|
|
106
|
+
}
|
|
107
|
+
/** Register for a free API key and cache it locally. */
|
|
108
|
+
export async function register() {
|
|
109
|
+
const result = await request('POST', '/register');
|
|
110
|
+
saveApiKey(result.api_key, result.key_prefix);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/** Submit scan results for a skill. */
|
|
114
|
+
export async function scan(payload) {
|
|
115
|
+
return request('POST', '/scan', { body: payload, auth: true });
|
|
116
|
+
}
|
|
117
|
+
/** Report a suspicious skill. */
|
|
118
|
+
export async function report(hash, reason, details) {
|
|
119
|
+
const body = { composite_hash: hash, reason };
|
|
120
|
+
if (details !== undefined)
|
|
121
|
+
body.details = details;
|
|
122
|
+
return request('POST', '/report', { body, auth: true });
|
|
123
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.skvil');
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'mcp-config.json');
|
|
6
|
+
/** In-memory cache to avoid repeated disk reads. */
|
|
7
|
+
let cachedApiKey;
|
|
8
|
+
let cachedBaseUrl;
|
|
9
|
+
function readConfig() {
|
|
10
|
+
try {
|
|
11
|
+
if (!existsSync(CONFIG_FILE))
|
|
12
|
+
return {};
|
|
13
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeConfig(config) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
22
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the API key from (in priority order):
|
|
26
|
+
* 1. SKVIL_API_KEY or SKVIL_KEDAVRA_API_KEY environment variable
|
|
27
|
+
* 2. Cached key in ~/.skvil/mcp-config.json
|
|
28
|
+
* 3. Legacy Python client config at ~/.skvil/config
|
|
29
|
+
*
|
|
30
|
+
* Result is memoized for the lifetime of the process.
|
|
31
|
+
*/
|
|
32
|
+
export function getApiKey() {
|
|
33
|
+
if (cachedApiKey !== undefined)
|
|
34
|
+
return cachedApiKey;
|
|
35
|
+
const envKey = process.env.SKVIL_API_KEY || process.env.SKVIL_KEDAVRA_API_KEY;
|
|
36
|
+
if (envKey) {
|
|
37
|
+
cachedApiKey = envKey;
|
|
38
|
+
return cachedApiKey;
|
|
39
|
+
}
|
|
40
|
+
const config = readConfig();
|
|
41
|
+
if (config.api_key) {
|
|
42
|
+
cachedApiKey = config.api_key;
|
|
43
|
+
return cachedApiKey;
|
|
44
|
+
}
|
|
45
|
+
// Try legacy Python client config (key=value format)
|
|
46
|
+
try {
|
|
47
|
+
const legacyPath = join(homedir(), '.skvil', 'config');
|
|
48
|
+
if (existsSync(legacyPath)) {
|
|
49
|
+
const content = readFileSync(legacyPath, 'utf-8');
|
|
50
|
+
const match = content.match(/^api_key\s*=\s*(.+)$/m);
|
|
51
|
+
if (match) {
|
|
52
|
+
const key = match[1].trim();
|
|
53
|
+
if (/^[a-zA-Z0-9_-]+$/.test(key)) {
|
|
54
|
+
cachedApiKey = key;
|
|
55
|
+
return cachedApiKey;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore read errors
|
|
62
|
+
}
|
|
63
|
+
cachedApiKey = null;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** Cache a newly registered API key for future use. */
|
|
67
|
+
export function saveApiKey(apiKey, keyPrefix) {
|
|
68
|
+
const config = readConfig();
|
|
69
|
+
config.api_key = apiKey;
|
|
70
|
+
config.key_prefix = keyPrefix;
|
|
71
|
+
config.registered_at = new Date().toISOString();
|
|
72
|
+
writeConfig(config);
|
|
73
|
+
cachedApiKey = apiKey;
|
|
74
|
+
}
|
|
75
|
+
/** Clear the memoized API key (used after registration). */
|
|
76
|
+
export function clearApiKeyCache() {
|
|
77
|
+
cachedApiKey = undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get the API base URL (override with SKVIL_API_URL or SKVIL_KEDAVRA_API_URL).
|
|
81
|
+
* Enforces HTTPS for non-localhost URLs to prevent credential leakage.
|
|
82
|
+
* Result is memoized for the lifetime of the process.
|
|
83
|
+
*/
|
|
84
|
+
export function getBaseUrl() {
|
|
85
|
+
if (cachedBaseUrl !== undefined)
|
|
86
|
+
return cachedBaseUrl;
|
|
87
|
+
const override = process.env.SKVIL_API_URL || process.env.SKVIL_KEDAVRA_API_URL;
|
|
88
|
+
if (override) {
|
|
89
|
+
const isLocalhost = /^https?:\/\/(localhost|127\.0\.0\.1|::1)(:\d+)?/.test(override);
|
|
90
|
+
if (!override.startsWith('https://') && !isLocalhost) {
|
|
91
|
+
process.stderr.write('[skvil-mcp] WARNING: SKVIL_API_URL must use HTTPS. Falling back to api.skvil.com.\n');
|
|
92
|
+
cachedBaseUrl = 'https://api.skvil.com';
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
cachedBaseUrl = override.replace(/\/+$/, '');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
cachedBaseUrl = 'https://api.skvil.com';
|
|
100
|
+
}
|
|
101
|
+
return cachedBaseUrl;
|
|
102
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { registerTools } from './tools.js';
|
|
5
|
+
import { VERSION } from './version.js';
|
|
6
|
+
process.on('unhandledRejection', (err) => {
|
|
7
|
+
process.stderr.write(`[skvil-mcp] unhandled rejection: ${err}\n`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: 'skvil',
|
|
12
|
+
version: VERSION,
|
|
13
|
+
});
|
|
14
|
+
registerTools(server);
|
|
15
|
+
try {
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
const shutdown = async () => {
|
|
19
|
+
await server.close();
|
|
20
|
+
process.exit(0);
|
|
21
|
+
};
|
|
22
|
+
process.on('SIGINT', shutdown);
|
|
23
|
+
process.on('SIGTERM', shutdown);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
process.stderr.write(`[skvil-mcp] failed to start: ${message}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as api from './api.js';
|
|
3
|
+
import { getApiKey } from './config.js';
|
|
4
|
+
const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
5
|
+
const hashSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(HASH_PATTERN, 'Must be "sha256:" followed by 64 hex characters')
|
|
8
|
+
.describe('SHA-256 composite hash of the skill (e.g. "sha256:4a2f8b...c81e")');
|
|
9
|
+
function formatScore(score) {
|
|
10
|
+
if (score >= 80)
|
|
11
|
+
return `${score.toFixed(1)} (safe)`;
|
|
12
|
+
if (score >= 50)
|
|
13
|
+
return `${score.toFixed(1)} (caution)`;
|
|
14
|
+
return `${score.toFixed(1)} (danger)`;
|
|
15
|
+
}
|
|
16
|
+
/** Register all skvil tools on the MCP server. */
|
|
17
|
+
export function registerTools(server) {
|
|
18
|
+
// ── skvil_verify ──────────────────────────────────────────────────────────
|
|
19
|
+
server.tool('skvil_verify', 'Check if an AI agent skill is safe before installing it. Returns reputation ' +
|
|
20
|
+
'score, risk level, certification status, and community scan data. ' +
|
|
21
|
+
'Use this to verify any skill by its SHA-256 composite hash.', { hash: hashSchema }, async ({ hash }) => {
|
|
22
|
+
try {
|
|
23
|
+
const result = await api.verify(hash);
|
|
24
|
+
if (!result.known) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: `**Unknown skill** (${hash})\n\n` +
|
|
30
|
+
'This skill has never been scanned by the Skvil network.\n' +
|
|
31
|
+
'It has no reputation data or certification.\n\n' +
|
|
32
|
+
'**Recommendation:** Do not install without scanning first.',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const score = result.reputation_score ?? 0;
|
|
38
|
+
const totalScans = result.total_scans ?? 0;
|
|
39
|
+
const lines = [
|
|
40
|
+
`**Skill verification: ${hash}**\n`,
|
|
41
|
+
`- **Reputation score:** ${formatScore(score)}`,
|
|
42
|
+
`- **Total community scans:** ${totalScans}`,
|
|
43
|
+
`- **Risk level:** ${result.risk_summary?.last_risk_level ?? 'unknown'}`,
|
|
44
|
+
];
|
|
45
|
+
if (result.certification) {
|
|
46
|
+
lines.push(`- **Certification:** ${result.certification}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
lines.push('- **Certification:** none');
|
|
50
|
+
}
|
|
51
|
+
if (result.confirmed_malicious) {
|
|
52
|
+
lines.push('\n**CONFIRMED MALICIOUS** — a Skvil admin has verified this skill is dangerous.');
|
|
53
|
+
lines.push('Do NOT install this skill.');
|
|
54
|
+
}
|
|
55
|
+
if (result.risk_summary) {
|
|
56
|
+
const f = result.risk_summary.findings_by_severity;
|
|
57
|
+
const critical = f.critical ?? 0;
|
|
58
|
+
const high = f.high ?? 0;
|
|
59
|
+
const medium = f.medium ?? 0;
|
|
60
|
+
const low = f.low ?? 0;
|
|
61
|
+
if (critical > 0 || high > 0) {
|
|
62
|
+
lines.push(`\n**Findings:** ${critical} critical, ${high} high, ${medium} medium, ${low} low`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (result.crucible) {
|
|
66
|
+
lines.push(`\n**Crucible behavioral analysis:** ${result.crucible.status}`);
|
|
67
|
+
lines.push(`- Behavioral score: ${result.crucible.score}`);
|
|
68
|
+
if (result.crucible.behavioral_findings.length > 0) {
|
|
69
|
+
const descriptions = result.crucible.behavioral_findings
|
|
70
|
+
.map((f) => f.description)
|
|
71
|
+
.join(', ');
|
|
72
|
+
lines.push(`- Findings: ${descriptions}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Decision recommendation
|
|
76
|
+
lines.push('\n**Recommendation:**');
|
|
77
|
+
if (result.confirmed_malicious) {
|
|
78
|
+
lines.push('Do NOT install. This skill has been confirmed malicious.');
|
|
79
|
+
}
|
|
80
|
+
else if (result.crucible?.status === 'malicious') {
|
|
81
|
+
lines.push('Do NOT install. Behavioral analysis detected malicious activity.');
|
|
82
|
+
}
|
|
83
|
+
else if (score >= 80 && result.certification) {
|
|
84
|
+
lines.push(`Safe to install. This skill is certified (${result.certification}) ` +
|
|
85
|
+
'with a strong reputation score.');
|
|
86
|
+
}
|
|
87
|
+
else if (score >= 60) {
|
|
88
|
+
lines.push('Likely safe, but not yet certified. Install with caution.');
|
|
89
|
+
}
|
|
90
|
+
else if (score < 40) {
|
|
91
|
+
lines.push('Do NOT install. Low reputation score indicates potential risk.');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
lines.push('Proceed with caution. Review findings before installing.');
|
|
95
|
+
}
|
|
96
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { content: [{ type: 'text', text: formatError('verify', error) }], isError: true };
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// ── skvil_stats ───────────────────────────────────────────────────────────
|
|
103
|
+
server.tool('skvil_stats', 'Get aggregate statistics from the Skvil community network: total skills ' +
|
|
104
|
+
'scanned, trusted count, critical findings, and certified skills.', {}, async () => {
|
|
105
|
+
try {
|
|
106
|
+
const result = await api.stats();
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: '**Skvil community statistics**\n\n' +
|
|
112
|
+
`- **Total skills scanned:** ${result.total}\n` +
|
|
113
|
+
`- **Trusted** (reputation >= 70): ${result.trusted}\n` +
|
|
114
|
+
`- **Critical findings:** ${result.critical}\n` +
|
|
115
|
+
`- **Certified:** ${result.certified}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { content: [{ type: 'text', text: formatError('stats', error) }], isError: true };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// ── skvil_certified ───────────────────────────────────────────────────────
|
|
125
|
+
server.tool('skvil_certified', 'List skills that have been verified and certified by Skvil admins. ' +
|
|
126
|
+
'Certified skills have been manually reviewed and registered for ' +
|
|
127
|
+
'tamper-proof verification. Returns up to 10 most recently certified ' +
|
|
128
|
+
'skills with their level (V1/V2/V3/Gold), reputation score, and ' +
|
|
129
|
+
'certification date.', {}, async () => {
|
|
130
|
+
try {
|
|
131
|
+
const result = await api.certified();
|
|
132
|
+
if (result.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: 'No skills are currently certified. Be the first to get certified!',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const lines = ['**Certified skills**\n'];
|
|
143
|
+
for (const skill of result) {
|
|
144
|
+
lines.push(`- **${skill.name}** — ${skill.level} | Score: ${formatScore(skill.reputation_score)} | ` +
|
|
145
|
+
`${skill.total_scans} scans | Certified: ${skill.certified_at}\n` +
|
|
146
|
+
` Hash: \`${skill.composite_hash}\``);
|
|
147
|
+
}
|
|
148
|
+
lines.push('\nAll certifications are registered for tamper-proof, publicly verifiable trust.');
|
|
149
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: 'text', text: formatError('certified', error) }],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// ── skvil_catalog ──────────────────────────────────────────────────────────
|
|
159
|
+
server.tool('skvil_catalog', 'Browse the full catalog of Skvil-certified AI agent skills with detailed ' +
|
|
160
|
+
'metadata: author, version, description, provider, agent platform, file ' +
|
|
161
|
+
'count, and install URL. Returns up to 100 skills. Use this to discover ' +
|
|
162
|
+
'safe skills available for installation.', {}, async () => {
|
|
163
|
+
try {
|
|
164
|
+
const result = await api.catalog();
|
|
165
|
+
if (result.length === 0) {
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: 'The skill catalog is empty. No certified skills available yet.',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const lines = [`**Skvil skill catalog** (${result.length} certified skills)\n`];
|
|
176
|
+
for (const skill of result) {
|
|
177
|
+
const parts = [`- **${skill.name}**`];
|
|
178
|
+
parts.push(` Level: ${skill.level} | Score: ${formatScore(skill.reputation_score)} | ${skill.total_scans} scans`);
|
|
179
|
+
if (skill.author)
|
|
180
|
+
parts.push(` Author: ${skill.author}`);
|
|
181
|
+
if (skill.version)
|
|
182
|
+
parts.push(` Version: ${skill.version}`);
|
|
183
|
+
if (skill.description)
|
|
184
|
+
parts.push(` ${skill.description}`);
|
|
185
|
+
if (skill.provider)
|
|
186
|
+
parts.push(` Provider: ${skill.provider}`);
|
|
187
|
+
if (skill.agent)
|
|
188
|
+
parts.push(` Agent: ${skill.agent}`);
|
|
189
|
+
parts.push(` Files: ${skill.file_count} | Certified: ${skill.certified_at}`);
|
|
190
|
+
if (skill.skill_url)
|
|
191
|
+
parts.push(` Install: ${skill.skill_url}`);
|
|
192
|
+
parts.push(` Hash: \`${skill.composite_hash}\``);
|
|
193
|
+
lines.push(parts.join('\n'));
|
|
194
|
+
}
|
|
195
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: 'text', text: formatError('catalog', error) }],
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// ── skvil_register ────────────────────────────────────────────────────────
|
|
205
|
+
server.tool('skvil_register', 'Register for a free Skvil API key. The key is automatically cached ' +
|
|
206
|
+
'locally for future use. No sign-up or account required. Other tools ' +
|
|
207
|
+
'(skvil_scan, skvil_report) will use the cached key automatically.', {}, async () => {
|
|
208
|
+
try {
|
|
209
|
+
const existing = getApiKey();
|
|
210
|
+
if (existing) {
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: 'text',
|
|
215
|
+
text: 'An API key is already configured.\n\n' +
|
|
216
|
+
'To register a new key, unset the `SKVIL_API_KEY` env var and ' +
|
|
217
|
+
'delete `~/.skvil/mcp-config.json`, then try again.',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const result = await api.register();
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: 'text',
|
|
227
|
+
text: '**API key registered successfully!**\n\n' +
|
|
228
|
+
`- **Key prefix:** ${result.key_prefix}...\n` +
|
|
229
|
+
`- **Tier:** ${result.tier}\n\n` +
|
|
230
|
+
'The key has been cached in `~/.skvil/mcp-config.json`.\n' +
|
|
231
|
+
'You can now use `skvil_scan` and `skvil_report`.',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: 'text', text: formatError('register', error) }],
|
|
239
|
+
isError: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
// ── skvil_report ──────────────────────────────────────────────────────────
|
|
244
|
+
server.tool('skvil_report', 'Report a suspicious or malicious AI agent skill to Skvil admins for review. ' +
|
|
245
|
+
'Requires an API key (use skvil_register first). Reports are reviewed by ' +
|
|
246
|
+
'admins and confirmed findings lead to certification revocation.', {
|
|
247
|
+
hash: hashSchema,
|
|
248
|
+
reason: z
|
|
249
|
+
.string()
|
|
250
|
+
.min(10)
|
|
251
|
+
.max(1000)
|
|
252
|
+
.describe('Why this skill is suspicious (10-1000 characters)'),
|
|
253
|
+
details: z
|
|
254
|
+
.string()
|
|
255
|
+
.max(5000)
|
|
256
|
+
.optional()
|
|
257
|
+
.describe('Additional details or evidence (optional, max 5000 characters)'),
|
|
258
|
+
}, async ({ hash, reason, details }) => {
|
|
259
|
+
try {
|
|
260
|
+
const result = await api.report(hash, reason, details);
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: '**Report submitted successfully**\n\n' +
|
|
266
|
+
`- **Report ID:** ${result.report_id}\n` +
|
|
267
|
+
`- **Status:** ${result.status}\n` +
|
|
268
|
+
`- **Skill hash:** ${hash}\n\n` +
|
|
269
|
+
'A Skvil admin will review this report. If confirmed, the skill ' +
|
|
270
|
+
'will be flagged as malicious and any existing certification ' +
|
|
271
|
+
'will be revoked.',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
return { content: [{ type: 'text', text: formatError('report', error) }], isError: true };
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
// ── skvil_scan ────────────────────────────────────────────────────────────
|
|
281
|
+
server.tool('skvil_scan', 'Submit security scan results for an AI agent skill to the Skvil reputation ' +
|
|
282
|
+
'network. Contributes to the community reputation score (EMA). Requires an ' +
|
|
283
|
+
'API key (use skvil_register first). The server recomputes the score from ' +
|
|
284
|
+
'findings — always provide accurate findings.', {
|
|
285
|
+
name: z.string().max(256).describe('Skill name'),
|
|
286
|
+
composite_hash: hashSchema,
|
|
287
|
+
file_count: z.number().int().min(0).max(10000).describe('Number of files in the skill'),
|
|
288
|
+
file_hashes: z
|
|
289
|
+
.record(z
|
|
290
|
+
.string()
|
|
291
|
+
.max(500)
|
|
292
|
+
.regex(/^[a-zA-Z0-9_\-./]+$/, 'Invalid file path'), z.string().regex(/^[a-f0-9]{64}$/, 'Must be 64 hex characters'))
|
|
293
|
+
.describe('Map of relative file paths to their SHA-256 hex hashes'),
|
|
294
|
+
score: z.number().int().min(0).max(100).describe('Computed security score (0-100)'),
|
|
295
|
+
risk_level: z.enum(['safe', 'caution', 'danger']).describe('Overall risk assessment'),
|
|
296
|
+
findings: z
|
|
297
|
+
.array(z.object({
|
|
298
|
+
severity: z.enum(['critical', 'high', 'medium', 'low']),
|
|
299
|
+
category: z.string().max(100),
|
|
300
|
+
description: z.string().max(1000),
|
|
301
|
+
file: z.string().max(500),
|
|
302
|
+
line: z.number().int().optional(),
|
|
303
|
+
}))
|
|
304
|
+
.max(500)
|
|
305
|
+
.default([])
|
|
306
|
+
.describe('Security findings detected in the skill'),
|
|
307
|
+
frontmatter: z
|
|
308
|
+
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
309
|
+
.optional()
|
|
310
|
+
.describe('SKILL.md frontmatter metadata (optional)'),
|
|
311
|
+
}, async (params) => {
|
|
312
|
+
try {
|
|
313
|
+
const result = await api.scan(params);
|
|
314
|
+
const lines = [
|
|
315
|
+
'**Scan submitted successfully**\n',
|
|
316
|
+
`- **Scan ID:** ${result.scan_id}`,
|
|
317
|
+
`- **Updated reputation:** ${formatScore(result.reputation_score)}`,
|
|
318
|
+
`- **Total community scans:** ${result.total_scans}`,
|
|
319
|
+
];
|
|
320
|
+
if (result.certification) {
|
|
321
|
+
lines.push(`- **Certification:** ${result.certification}`);
|
|
322
|
+
}
|
|
323
|
+
lines.push('\nYour scan contributes to the community reputation via exponential ' +
|
|
324
|
+
'moving average (EMA). Thank you for helping secure the AI skill ecosystem.');
|
|
325
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
return { content: [{ type: 'text', text: formatError('scan', error) }], isError: true };
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function formatError(tool, error) {
|
|
333
|
+
if (error instanceof api.SkvilApiError) {
|
|
334
|
+
if (error.status === 429) {
|
|
335
|
+
return `**Rate limit exceeded** — ${error.detail}\nTry again later.`;
|
|
336
|
+
}
|
|
337
|
+
if (error.status === 401) {
|
|
338
|
+
return (`**Authentication required** — ${error.detail}\n` +
|
|
339
|
+
'Use `skvil_register` to get a free API key.');
|
|
340
|
+
}
|
|
341
|
+
const safeDetail = error.detail.slice(0, 500);
|
|
342
|
+
return `**Error in skvil_${tool}** (HTTP ${error.status})\n${safeDetail}`;
|
|
343
|
+
}
|
|
344
|
+
if (error instanceof Error) {
|
|
345
|
+
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
|
|
346
|
+
return '**Timeout** — the Skvil API did not respond in time. Try again.';
|
|
347
|
+
}
|
|
348
|
+
return `**Error in skvil_${tool}**\n${error.message}`;
|
|
349
|
+
}
|
|
350
|
+
return `**Unexpected error in skvil_${tool}**\n${String(error)}`;
|
|
351
|
+
}
|
package/dist/types.js
ADDED
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skvil/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for the Skvil security scanner — verify, scan, and report AI agent skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skvil-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"lint": "eslint src/",
|
|
18
|
+
"format": "prettier --write 'src/**/*.ts'",
|
|
19
|
+
"format:check": "prettier --check 'src/**/*.ts'",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "echo 'No tests configured yet' && exit 0",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"skvil",
|
|
31
|
+
"security",
|
|
32
|
+
"ai-agents",
|
|
33
|
+
"ai-skills",
|
|
34
|
+
"scanner"
|
|
35
|
+
],
|
|
36
|
+
"author": "Skvil <terminal@skvil.com>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/Skvil-IA/skvil-mcp.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://skvil.com",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/Skvil-IA/skvil-mcp/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
51
|
+
"zod": "^3.24.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.0.0",
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"eslint": "^9.0.0",
|
|
57
|
+
"@eslint/js": "^9.0.0",
|
|
58
|
+
"typescript-eslint": "^8.0.0",
|
|
59
|
+
"prettier": "^3.4.0"
|
|
60
|
+
}
|
|
61
|
+
}
|